fix(video): remux HEVC to mp4/mov as hvc1 via hevc_mp4toannexb (#15809)

This commit is contained in:
Alexander Piskun
2026-08-27 04:28:50 +04:00
committed by GitHub
parent 5653b4ac8e
commit d8e7bbc9d5
2 changed files with 200 additions and 2 deletions
+35 -2
View File
@@ -1,3 +1,4 @@
from av.bitstream import BitStreamFilterContext
from av.container import InputContainer
from av.subtitles.stream import SubtitleStream
from av.video.reformatter import ColorPrimaries, ColorRange, ColorTrc
@@ -96,6 +97,31 @@ def video_stream_bit_depth(stream) -> int:
return max(component.bits for component in stream.format.components)
def isobmff_hevc_filter(output_container, stream, out_stream):
"""Apple players need the 'hvc1' sample entry, not FFmpeg's default 'hev1'. Annex B input without
extradata makes the muxer build hvcC from the first packet and strip in-band parameter sets;
'hvc1' sources already have a complete hvcC and only need the tag PyAV reset."""
if output_container.format.name not in ("mp4", "mov") or stream.codec.canonical_name != "hevc":
return None
try:
codec_tag = stream.codec_context.codec_tag
except UnicodeDecodeError:
codec_tag = ""
if codec_tag == "hvc1":
out_stream.codec_context.codec_tag = "hvc1"
return None
hevc_filter = BitStreamFilterContext("hevc_mp4toannexb", stream, out_stream)
out_stream.codec_context.codec_tag = "hvc1"
out_stream.codec_context.extradata = None
return hevc_filter
def filter_hevc_packet(hevc_filter, packet):
if packet.has_sidedata("new_extradata"):
raise ValueError("HEVC with multiple sample descriptions cannot be remuxed as hvc1; re-encode it instead")
return hevc_filter.filter(packet)
def last_decodable_audio_stream(container: InputContainer):
"""Streams FFmpeg has no decoder for have no codec context, and decoding their
packets crashes the process (e.g. APAC spatial-audio track in iPhone)."""
@@ -601,19 +627,26 @@ class VideoFromFile(VideoInput):
# Add streams to the new container. Streams with no codec context cannot be used as an output template.
stream_map = {}
hevc_filters = {}
for stream in streams:
if isinstance(stream, (av.VideoStream, av.AudioStream, SubtitleStream)):
if stream.codec_context is None:
logging.warning("Skipping %s stream %d with unsupported codec", stream.type, stream.index)
continue
out_stream = output_container.add_stream_from_template(template=stream, opaque=True)
hevc_filter = isobmff_hevc_filter(output_container, stream, out_stream)
if hevc_filter is not None:
hevc_filters[stream] = hevc_filter
stream_map[stream] = out_stream
# Write packets to the new container
for packet in container.demux():
if packet.stream in stream_map and packet.dts is not None:
packet.stream = stream_map[packet.stream]
output_container.mux(packet)
out_stream = stream_map[packet.stream]
hevc_filter = hevc_filters.get(packet.stream)
for out_packet in filter_hevc_packet(hevc_filter, packet) if hevc_filter else (packet,):
out_packet.stream = out_stream
output_container.mux(out_packet)
def _save_transcoded(
self,