@@ -71,8 +71,7 @@ def get_open_write_kwargs(
7171 is_write_to_buffer = isinstance (dest , io .BytesIO )
7272 open_kwargs = {"mode" : "w" }
7373
74- if is_write_to_buffer :
75- # Set output format explicitly, since it cannot be inferred from file extension
74+ if is_write_to_buffer or to_format != VideoContainer .AUTO :
7675 if to_format == VideoContainer .AUTO :
7776 to_format = container_format .lower ()
7877 elif isinstance (to_format , VideoContainer ):
@@ -81,7 +80,7 @@ def get_open_write_kwargs(
8180 to_format = to_format .lower ()
8281 open_kwargs ["format" ] = container_to_output_format (to_format )
8382
84- output_format = open_kwargs ["format" ] if is_write_to_buffer else os .path .splitext (dest )[1 ].lower ().lstrip ("." )
83+ output_format = open_kwargs ["format" ] if "format" in open_kwargs else os .path .splitext (dest )[1 ].lower ().lstrip ("." )
8584 if output_format in ("mov" , "mp4" ):
8685 # Preserve custom metadata tags (workflow, prompt, extra_pnginfo) in isobmff.
8786 movflags = "use_metadata_tags" if is_write_to_buffer else "use_metadata_tags+faststart"
@@ -617,10 +616,8 @@ def _save_remuxed(
617616 ) -> bool :
618617 streams = container .streams
619618 with av .open (path , ** open_kwargs ) as output_container :
620- # Add metadata before writing any streams
621619 write_output_metadata (container , output_container , metadata )
622620
623- # Add streams to the new container. Streams with no codec context cannot be used as an output template.
624621 stream_map = {}
625622 for stream in streams :
626623 if isinstance (stream , (av .VideoStream , av .AudioStream , SubtitleStream )):
@@ -647,7 +644,6 @@ def _save_remuxed(
647644 return False
648645 stream_map [stream ] = out_stream
649646
650- # Write packets to the new container
651647 for packet in container .demux ():
652648 if packet .stream in stream_map and packet .dts is not None :
653649 packet .stream = stream_map [packet .stream ]
@@ -683,6 +679,7 @@ def _save_transcoded(
683679 audio_stream = last_decodable_audio_stream (container )
684680 source_color_space = video_stream_color_space (video_stream )
685681 preserve_source_color = source_color_space is not None
682+ normalize_color_range = video_stream .color_range == ColorRange .JPEG and source_color_space not in HDR_COLOR_TRANSFERS
686683 if color_space in HDR_COLOR_TRANSFERS or source_color_space in HDR_COLOR_TRANSFERS :
687684 bit_depth = max (bit_depth , 10 )
688685 pix_fmt = "yuv420p10le" if bit_depth >= 10 else "yuv420p"
@@ -712,15 +709,10 @@ def _save_transcoded(
712709 if duration :
713710 duration_cap = math .ceil (duration * sample_rate )
714711
715- # Subtitles are remuxed untouched: there is no subtitle encoder binding, so a stream the
716- # output container cannot store as-is is dropped with a warning naming it, exactly like
717- # the remux path does. Streams FFmpeg has no decoder for cannot template a new stream.
718712 subtitle_streams = [s for s in container .streams .subtitles if s .codec_context is not None ]
719713 streams = [video_stream ] if audio_stream is None else [video_stream , audio_stream ]
720714 streams += subtitle_streams
721715 subtitle_map = {}
722- # Subtitle packets that arrive before the first kept video frame: the output is not open
723- # yet and the pts rebase offset is not known, so they wait here rather than being lost.
724716 pending_subtitles = []
725717 pts_step = max (1 , int (round ((1 / rate ) / video_stream .time_base )))
726718 video_done = False
@@ -780,15 +772,12 @@ def drain_audio(final=False):
780772 return cap
781773
782774 def mux_subtitle (packet ):
783- """Remux one subtitle packet, rebased onto the trimmed timeline the video was rebased to."""
784775 out_stream = subtitle_map .get (packet .stream )
785776 if out_stream is None or packet .dts is None or packet .pts is None or packet .time_base is None :
786777 return
787778 start = float (packet .pts * packet .time_base )
788779 if start < start_time or (duration and start >= start_time + duration ):
789780 return
790- # the video's own rebase offset, so subtitles stay in sync with it rather than
791- # with the requested start (a seek lands on the preceding keyframe)
792781 offset_ticks = video_pts_offset if video_pts_offset is not None else start_pts
793782 shift = int (round (float (offset_ticks * video_stream .time_base ) / packet .time_base ))
794783 packet .pts -= shift
@@ -799,7 +788,6 @@ def mux_subtitle(packet):
799788 output .mux (packet )
800789
801790 def flush_subtitles ():
802- # only once the first video frame fixed the rebase offset
803791 if output is None or not pending_subtitles or last_video_pts is None :
804792 return
805793 while pending_subtitles :
@@ -853,6 +841,8 @@ def flush_subtitles():
853841 out_video .options = video_encoder_options (output_codec , crf )
854842 if preserve_source_color :
855843 copy_color_properties (video_stream , out_video .codec_context )
844+ if normalize_color_range :
845+ out_video .codec_context .color_range = ColorRange .MPEG
856846 elif color_space is not None :
857847 set_video_color_properties (out_video .codec_context , color_space )
858848 # source pts pass through (rebased to 0), so variable frame rate survives
@@ -897,13 +887,14 @@ def flush_subtitles():
897887 rotation_filter = (g_src , g_sink )
898888 rotation_filter [0 ].push (frame )
899889 frame = rotation_filter [1 ].pull ()
900- if frame .color_range == ColorRange .JPEG and not preserve_source_color :
901- # compress full-range sources (yuvj/MJPEG) to limited range
890+ if frame .color_range == ColorRange .JPEG and normalize_color_range :
902891 frame = frame .reformat (format = pix_fmt , src_color_range = "JPEG" , dst_color_range = "MPEG" )
903892 else :
904893 frame = frame .reformat (format = pix_fmt )
905894 if preserve_source_color :
906895 copy_color_properties (video_stream , frame )
896+ if normalize_color_range :
897+ frame .color_range = ColorRange .MPEG
907898 elif color_space is not None :
908899 set_video_color_properties (frame , color_space )
909900 frame_output_end = None
0 commit comments