aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py89
1 files changed, 67 insertions, 22 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index 540c174..d79674d 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -126,7 +126,7 @@ from meshbay_node.chat import FORMAT_SEALED_V1, ReplayedMessage
from meshbay_node.transport.wire import index_sync_message
from meshbay_node.indexer import GroupIndex
from meshbay_node.indexer.indexer import DirectoryIndexer
-from meshbay_node import linkpreview, ops, platform
+from meshbay_node import hwaccel, linkpreview, ops, platform
from meshbay_node import transfers as transfers_mod
from meshbay_node import uploads as uploads_mod
from meshbay_node.transfers import TransferSlots
@@ -6265,20 +6265,33 @@ class WebRTCPeerSession:
if transcode_video:
log.info("stream: re-encoding %s (%s) to H264", entry.name,
raw_video_codec)
- # -pix_fmt yuv420p: a 10-bit or 4:4:4 HEVC source (common for HDR
- # WEB-DLs) fails "-profile:v high" outright otherwise — libx264's
- # High profile is 8-bit 4:2:0 only. Downsampling loses nothing a
- # browser could show anyway (MSE/HTML5 video has no HDR path).
- codec_args = ["-c:v", "libx264", "-pix_fmt", "yuv420p",
- "-profile:v", "high", "-level", "4.1",
- "-preset", "veryfast", "-crf", "21"]
+ # Where the re-encode runs, and with which arguments — both live
+ # in hwaccel.py now, including the 8-bit downsampling a 10-bit HDR
+ # source needs before either encoder will take it. `modes_for` has
+ # measured this machine by encoding on it and returns the ladder to
+ # try, always ending in libx264: a node with no usable VA-API does
+ # exactly what it did before this existed, and a Celeron with an
+ # iGPU stops being a machine where `transcode_incompatible_video`
+ # has to be turned off to keep streaming watchable.
+ modes = await hwaccel.modes_for(raw_video_codec)
+ hw = await hwaccel.encoder()
# Must match "-profile:v high -level 4.1" byte-for-byte (avc1.<profile
# hex><constraint><level hex>) — the client checks this string with
# MediaSource.isTypeSupported before trusting a single byte of the
- # stream, so a mismatch here fails exactly the check this exists to pass.
+ # stream, so a mismatch here fails exactly the check this exists to
+ # pass. Both encoders get those two arguments, spelled the same way,
+ # from hwaccel._PROFILE_ARGS — one place, so they cannot drift.
codec_str = "avc1.640029,mp4a.40.2" if has_audio else "avc1.640029"
else:
- codec_args = ["-c:v", "copy"]
+ modes, hw = [hwaccel.SW], None
+
+ def video_args(mode: str) -> list[str]:
+ return (hwaccel.codec_args(mode, hw) if transcode_video
+ else ["-c:v", "copy"])
+
+ # The audio half does not change with the video encoder, and is never a
+ # copy — see _probe_video for why.
+ audio_args: list[str] = []
# Which audio track. A dubbed film carries several and the first one is
# not a neutral default — it is whatever the person who muxed the file
# happened to put first, which across a real library is overwhelmingly
@@ -6302,7 +6315,7 @@ class WebRTCPeerSession:
# appended — isTypeSupported() only checks the codec string, so
# the failure doesn't surface until playback, as a SourceBuffer
# forced out of its MediaSource with no further explanation.
- codec_args += ["-c:a", "aac", "-ac", "2", "-b:a", "192k"]
+ audio_args = ["-c:a", "aac", "-ac", "2", "-b:a", "192k"]
# Where that seek lands, measured with the mapping this stream will
# use. It has to be here rather than beside `seek_args` above: the
# landing point depends on which audio track is mapped, because the
@@ -6316,16 +6329,45 @@ class WebRTCPeerSession:
landed = await _seek_lands_at(file_path, requested, map_args)
if landed is not None:
start = landed
- proc = await asyncio.create_subprocess_exec(
- platform.ffmpeg_cmd(), "-hide_banner", "-loglevel", "error",
- *seek_args,
- "-i", str(file_path),
- *map_args,
- *codec_args,
- "-movflags", "frag_keyframe+empty_moov+default_base_moof",
- "-f", "mp4", "pipe:1",
- stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
- )
+ # One spawn per mode, and only ever more than one when hwaccel.py found
+ # a working GPU. **What a mode is tried against is the file itself**:
+ # a test encode proves the encoder, and nothing proves the GPU can
+ # decode *this* source until it is asked to — iHD has no MPEG-4 Part 2
+ # decoder at all, so an Xvid .avi fails the full-hardware mode and
+ # nothing about the machine could have predicted it.
+ #
+ # The failure is silent and instant: ffmpeg writes its complaint to
+ # stderr and exits, so stdout reaches EOF with nothing on it. That is
+ # the signal read here, before `stream_init` is sent and therefore
+ # before the client has been told anything it would have to be told
+ # again. The first segment is kept and handed to the loop below rather
+ # than re-read, since the process it came from is still running.
+ #
+ # The last mode is spawned and trusted, which is what keeps the
+ # single-mode path — every node without a GPU, and every copied stream
+ # — byte-for-byte what it was: no extra read, no extra wait.
+ first_segment = b""
+ for attempt, mode in enumerate(modes):
+ proc = await asyncio.create_subprocess_exec(
+ platform.ffmpeg_cmd(), "-hide_banner", "-loglevel", "error",
+ *hwaccel.input_args(mode, hw),
+ *seek_args,
+ "-i", str(file_path),
+ *map_args,
+ *video_args(mode), *audio_args,
+ "-movflags", "frag_keyframe+empty_moov+default_base_moof",
+ "-f", "mp4", "pipe:1",
+ stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
+ )
+ if attempt == len(modes) - 1:
+ break
+ first_segment = await proc.stdout.read(STREAM_SEGMENT_SIZE)
+ if first_segment:
+ break
+ err = (await proc.stderr.read()).decode("utf-8", "replace").strip()
+ await proc.wait()
+ hwaccel.demote(raw_video_codec, mode,
+ err.splitlines()[0] if err else "no output")
self._send({
"type": MNP.STREAM_INIT,
@@ -6404,7 +6446,10 @@ class WebRTCPeerSession:
log.info("Stream stopped by peer=%s after %d segments",
(self._user_id or "?")[:8], index)
break
- data = await proc.stdout.read(STREAM_SEGMENT_SIZE)
+ if first_segment:
+ data, first_segment = first_segment, b""
+ else:
+ data = await proc.stdout.read(STREAM_SEGMENT_SIZE)
if not data:
break
# Same derivation as a file chunk, indexed by segment: one