Converted the video player to be async
This commit is contained in:
parent
f9bb34fce7
commit
70cff70247
5 changed files with 117 additions and 96 deletions
|
|
@ -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
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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'))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue