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.') raise web.HTTPBadRequest(reason='file is required.')
return web.Response( 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, download_path=self.config.download_path,
file=file file=file
), ),

View file

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

View file

@ -1,3 +1,4 @@
import asyncio
import hashlib import hashlib
import logging import logging
import os import os
@ -37,69 +38,80 @@ class Segments:
else: else:
startTime: float = '{:.6f}'.format((self.segment_duration * self.segment_index)) startTime: float = '{:.6f}'.format((self.segment_duration * self.segment_index))
ffmpegCmd = [] fargs = []
ffmpegCmd.append('ffmpeg') fargs.append('-xerror')
ffmpegCmd.append('-xerror') fargs.append('-hide_banner')
ffmpegCmd.append('-hide_banner') fargs.append('-loglevel')
ffmpegCmd.append('-loglevel') fargs.append('error')
ffmpegCmd.append('error')
ffmpegCmd.append('-ss') fargs.append('-ss')
ffmpegCmd.append(str(startTime if startTime else '0.00000')) fargs.append(str(startTime if startTime else '0.00000'))
ffmpegCmd.append('-t') fargs.append('-t')
ffmpegCmd.append(str('{:.6f}'.format(self.segment_duration))) fargs.append(str('{:.6f}'.format(self.segment_duration)))
ffmpegCmd.append('-copyts') fargs.append('-copyts')
ffmpegCmd.append('-i') fargs.append('-i')
ffmpegCmd.append(f'file:{tmpFile}') fargs.append(f'file:{tmpFile}')
ffmpegCmd.append('-map_metadata') fargs.append('-map_metadata')
ffmpegCmd.append('-1') fargs.append('-1')
ffmpegCmd.append('-pix_fmt') fargs.append('-pix_fmt')
ffmpegCmd.append('yuv420p') fargs.append('yuv420p')
ffmpegCmd.append('-g') fargs.append('-g')
ffmpegCmd.append('52') fargs.append('52')
ffmpegCmd.append('-map') fargs.append('-map')
ffmpegCmd.append('0:v:0') fargs.append('0:v:0')
ffmpegCmd.append('-strict') fargs.append('-strict')
ffmpegCmd.append('-2') fargs.append('-2')
ffmpegCmd.append('-codec:v') fargs.append('-codec:v')
ffmpegCmd.append('libx264' if self.vconvert else 'copy') fargs.append('libx264' if self.vconvert else 'copy')
if self.vconvert: if self.vconvert:
ffmpegCmd.append('-crf') fargs.append('-crf')
ffmpegCmd.append('23') fargs.append('23')
ffmpegCmd.append('-preset:v') fargs.append('-preset:v')
ffmpegCmd.append('fast') fargs.append('fast')
ffmpegCmd.append('-level') fargs.append('-level')
ffmpegCmd.append('4.1') fargs.append('4.1')
ffmpegCmd.append('-profile:v') fargs.append('-profile:v')
ffmpegCmd.append('baseline') fargs.append('baseline')
# audio section. # audio section.
ffmpegCmd.append('-map') fargs.append('-map')
ffmpegCmd.append('0:a:0') fargs.append('0:a:0')
ffmpegCmd.append('-codec:a') fargs.append('-codec:a')
ffmpegCmd.append('aac' if self.aconvert else 'copy') fargs.append('aac' if self.aconvert else 'copy')
if self.aconvert: if self.aconvert:
ffmpegCmd.append('-b:a') fargs.append('-b:a')
ffmpegCmd.append('192k') fargs.append('192k')
ffmpegCmd.append('-ar') fargs.append('-ar')
ffmpegCmd.append('22050') fargs.append('22050')
ffmpegCmd.append('-ac') fargs.append('-ac')
ffmpegCmd.append('2') fargs.append('2')
ffmpegCmd.append('-sn') fargs.append('-sn')
ffmpegCmd.append('-muxdelay') fargs.append('-muxdelay')
ffmpegCmd.append('0') fargs.append('0')
ffmpegCmd.append('-f') fargs.append('-f')
ffmpegCmd.append('mpegts') fargs.append('mpegts')
ffmpegCmd.append('pipe:1') fargs.append('pipe:1')
LOG.debug(f'Streaming {realFile} segment {self.segment_index}.' + ' '.join(ffmpegCmd)) LOG.debug(f"Streaming '{realFile}' segment '{self.segment_index}'. " + " ".join(fargs))
proc = subprocess.run(ffmpegCmd, stdout=subprocess.PIPE)
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. Python wrapper for ffprobe command line tool. ffprobe must exist in the path.
""" """
import asyncio
import functools import functools
import json import json
import logging
import operator import operator
import os import os
import pipes import pipes
@ -178,55 +180,60 @@ class FFProbe:
def __init__(self, path_to_video): def __init__(self, path_to_video):
self.path_to_video = path_to_video self.path_to_video = path_to_video
async def run(self):
try: try:
with open(os.devnull, 'w') as tempf: 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: except FileNotFoundError:
raise IOError('ffprobe not found.') raise IOError('ffprobe not found.')
if os.path.isfile(self.path_to_video): if not os.path.isfile(self.path_to_video):
cmd: list = [ raise IOError(f"No such media file '{self.path_to_video}'.")
"ffprobe -v quiet -of json -show_format -show_streams " +
pipes.quote(self.path_to_video)
]
p = subprocess.Popen( args = [
args=cmd, '-v', 'quiet',
stdout=subprocess.PIPE, '-of', 'json',
stderr=subprocess.PIPE, '-show_streams',
shell=True '-show_format',
) self.path_to_video,
p.wait() ]
data, err = p.communicate() p = await asyncio.create_subprocess_exec(
if p.returncode == 0: 'ffprobe', *args,
parsed: dict = json.loads(data.decode('utf-8')) stdout=asyncio.subprocess.PIPE,
else: stderr=asyncio.subprocess.PIPE,
raise FFProbeError(f"FFProbe error: {err}") )
stream = False exitCode = await p.wait()
self.streams = []
self.video = []
self.audio = []
self.subtitle = []
self.attachment = []
for stream in parsed['streams'] if 'streams' in parsed else []: data, err = await p.communicate()
self.streams.append(FFStream(stream)) if 0 == exitCode:
parsed: dict = json.loads(data.decode('utf-8'))
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)
else: 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): def __repr__(self):
return "<FFprobe: {metadata}, {video}, {audio}, {subtitle}, {attachment}>".format(**vars(self)) return "<FFprobe: {metadata}, {video}, {audio}, {subtitle}, {attachment}>".format(**vars(self))

View file

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