Converted the video player to be async

This commit is contained in:
ArabCoders 2024-03-25 23:15:37 +03:00
parent f9bb34fce7
commit 70cff70247
5 changed files with 117 additions and 96 deletions

View file

@ -417,7 +417,7 @@ class Main:
raise web.HTTPBadRequest(reason='file is required.')
return web.Response(
text=M3u8(url=f"{self.config.url_host}{self.config.url_prefix}").make_stream(
text=await M3u8(url=f"{self.config.url_host}{self.config.url_prefix}").make_stream(
download_path=self.config.download_path,
file=file
),

View file

@ -16,19 +16,20 @@ class M3u8:
self.url = url
self.segment_duration = float(segment_duration)
def make_stream(self, download_path: str, file: str):
async def make_stream(self, download_path: str, file: str):
realFile: str = calcDownloadPath(basePath=download_path, folder=file, createPath=False)
if not os.path.exists(realFile):
raise Exception(f"File {realFile} does not exist.")
raise Exception(f"File '{realFile}' does not exist.")
try:
ffprobe = FFProbe(realFile)
await ffprobe.run()
except UnicodeDecodeError as e:
pass
if not 'duration' in ffprobe.metadata:
raise Exception(f"Unable to get {realFile} duration.")
raise Exception(f"Unable to get '{realFile}' duration.")
duration: float = float(ffprobe.metadata.get('duration'))

View file

@ -1,3 +1,4 @@
import asyncio
import hashlib
import logging
import os
@ -37,69 +38,80 @@ class Segments:
else:
startTime: float = '{:.6f}'.format((self.segment_duration * self.segment_index))
ffmpegCmd = []
ffmpegCmd.append('ffmpeg')
ffmpegCmd.append('-xerror')
ffmpegCmd.append('-hide_banner')
ffmpegCmd.append('-loglevel')
ffmpegCmd.append('error')
fargs = []
fargs.append('-xerror')
fargs.append('-hide_banner')
fargs.append('-loglevel')
fargs.append('error')
ffmpegCmd.append('-ss')
ffmpegCmd.append(str(startTime if startTime else '0.00000'))
ffmpegCmd.append('-t')
ffmpegCmd.append(str('{:.6f}'.format(self.segment_duration)))
fargs.append('-ss')
fargs.append(str(startTime if startTime else '0.00000'))
fargs.append('-t')
fargs.append(str('{:.6f}'.format(self.segment_duration)))
ffmpegCmd.append('-copyts')
fargs.append('-copyts')
ffmpegCmd.append('-i')
ffmpegCmd.append(f'file:{tmpFile}')
ffmpegCmd.append('-map_metadata')
ffmpegCmd.append('-1')
fargs.append('-i')
fargs.append(f'file:{tmpFile}')
fargs.append('-map_metadata')
fargs.append('-1')
ffmpegCmd.append('-pix_fmt')
ffmpegCmd.append('yuv420p')
ffmpegCmd.append('-g')
ffmpegCmd.append('52')
fargs.append('-pix_fmt')
fargs.append('yuv420p')
fargs.append('-g')
fargs.append('52')
ffmpegCmd.append('-map')
ffmpegCmd.append('0:v:0')
ffmpegCmd.append('-strict')
ffmpegCmd.append('-2')
fargs.append('-map')
fargs.append('0:v:0')
fargs.append('-strict')
fargs.append('-2')
ffmpegCmd.append('-codec:v')
ffmpegCmd.append('libx264' if self.vconvert else 'copy')
fargs.append('-codec:v')
fargs.append('libx264' if self.vconvert else 'copy')
if self.vconvert:
ffmpegCmd.append('-crf')
ffmpegCmd.append('23')
ffmpegCmd.append('-preset:v')
ffmpegCmd.append('fast')
ffmpegCmd.append('-level')
ffmpegCmd.append('4.1')
ffmpegCmd.append('-profile:v')
ffmpegCmd.append('baseline')
fargs.append('-crf')
fargs.append('23')
fargs.append('-preset:v')
fargs.append('fast')
fargs.append('-level')
fargs.append('4.1')
fargs.append('-profile:v')
fargs.append('baseline')
# audio section.
ffmpegCmd.append('-map')
ffmpegCmd.append('0:a:0')
ffmpegCmd.append('-codec:a')
ffmpegCmd.append('aac' if self.aconvert else 'copy')
fargs.append('-map')
fargs.append('0:a:0')
fargs.append('-codec:a')
fargs.append('aac' if self.aconvert else 'copy')
if self.aconvert:
ffmpegCmd.append('-b:a')
ffmpegCmd.append('192k')
ffmpegCmd.append('-ar')
ffmpegCmd.append('22050')
ffmpegCmd.append('-ac')
ffmpegCmd.append('2')
fargs.append('-b:a')
fargs.append('192k')
fargs.append('-ar')
fargs.append('22050')
fargs.append('-ac')
fargs.append('2')
ffmpegCmd.append('-sn')
fargs.append('-sn')
ffmpegCmd.append('-muxdelay')
ffmpegCmd.append('0')
ffmpegCmd.append('-f')
ffmpegCmd.append('mpegts')
ffmpegCmd.append('pipe:1')
fargs.append('-muxdelay')
fargs.append('0')
fargs.append('-f')
fargs.append('mpegts')
fargs.append('pipe:1')
LOG.debug(f'Streaming {realFile} segment {self.segment_index}.' + ' '.join(ffmpegCmd))
proc = subprocess.run(ffmpegCmd, stdout=subprocess.PIPE)
LOG.debug(f"Streaming '{realFile}' segment '{self.segment_index}'. " + " ".join(fargs))
return proc.stdout
proc = await asyncio.subprocess.create_subprocess_exec(
'ffmpeg', *fargs,
stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
data, err = await proc.communicate()
if 0 != proc.returncode:
LOG.error(f'Failed to stream {realFile} segment {self.segment_index}. {err.decode("utf-8")}')
raise Exception(f'Failed to stream {realFile} segment {self.segment_index}.')
return data

View file

@ -1,8 +1,10 @@
"""
Python wrapper for ffprobe command line tool. ffprobe must exist in the path.
"""
import asyncio
import functools
import json
import logging
import operator
import os
import pipes
@ -178,55 +180,60 @@ class FFProbe:
def __init__(self, path_to_video):
self.path_to_video = path_to_video
async def run(self):
try:
with open(os.devnull, 'w') as tempf:
subprocess.check_call(["ffprobe", "-h"], stdout=tempf, stderr=tempf)
await asyncio.create_subprocess_exec(
"ffprobe", "-h", stdout=tempf, stderr=tempf
)
except FileNotFoundError:
raise IOError('ffprobe not found.')
if os.path.isfile(self.path_to_video):
cmd: list = [
"ffprobe -v quiet -of json -show_format -show_streams " +
pipes.quote(self.path_to_video)
]
if not os.path.isfile(self.path_to_video):
raise IOError(f"No such media file '{self.path_to_video}'.")
p = subprocess.Popen(
args=cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True
)
p.wait()
data, err = p.communicate()
if p.returncode == 0:
parsed: dict = json.loads(data.decode('utf-8'))
else:
raise FFProbeError(f"FFProbe error: {err}")
args = [
'-v', 'quiet',
'-of', 'json',
'-show_streams',
'-show_format',
self.path_to_video,
]
p = await asyncio.create_subprocess_exec(
'ffprobe', *args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stream = False
self.streams = []
self.video = []
self.audio = []
self.subtitle = []
self.attachment = []
exitCode = await p.wait()
for stream in parsed['streams'] if 'streams' in parsed else []:
self.streams.append(FFStream(stream))
self.metadata = parsed['format'] if 'format' in parsed else {}
for stream in self.streams:
if stream.is_audio():
self.audio.append(stream)
elif stream.is_video():
self.video.append(stream)
elif stream.is_subtitle():
self.subtitle.append(stream)
elif stream.is_attachment():
self.attachment.append(stream)
data, err = await p.communicate()
if 0 == exitCode:
parsed: dict = json.loads(data.decode('utf-8'))
else:
raise IOError('No such media file ' + self.path_to_video)
raise FFProbeError(f"FFProbe return with non-0 exit code. '{err.decode('utf-8')}'")
stream = False
self.streams = []
self.video = []
self.audio = []
self.subtitle = []
self.attachment = []
for stream in parsed.get('streams', []):
self.streams.append(FFStream(stream))
self.metadata = parsed.get('format', {})
for stream in self.streams:
if stream.is_audio():
self.audio.append(stream)
elif stream.is_video():
self.video.append(stream)
elif stream.is_subtitle():
self.subtitle.append(stream)
elif stream.is_attachment():
self.attachment.append(stream)
def __repr__(self):
return "<FFprobe: {metadata}, {video}, {audio}, {subtitle}, {attachment}>".format(**vars(self))

View file

@ -72,6 +72,7 @@ const prepareVideoPlayer = () => {
enableWorker: true,
lowLatencyMode: true,
backBufferLength: 90,
fragLoadingTimeOut: 200000,
});
hls.value.loadSource(props.link)