@@ -77,15 +77,18 @@ STABLE_TORCH_LIBRARY_FRAGMENT(torchcodec_ns, m) {
7777 " _blocks_create_demuxer(str filename, int? stream_index=None) -> Tensor" );
7878 m.def (" _blocks_demuxer_next_packet(Tensor(a!) demuxer) -> (Tensor, bool)" );
7979 m.def (
80- " _blocks_create_packet_decoder(Tensor demuxer, *, int? num_threads=None, str device=\" cpu\" , str device_variant=\" default\" ) -> Tensor" );
80+ " _blocks_create_packet_decoder(Tensor demuxer, *, int? num_threads=None, str device=\" cpu\" , str device_variant=\" default\" , str output_dtype= \" uint8 \" ) -> Tensor" );
8181 m.def (
8282 " _blocks_packet_decoder_send_packet(Tensor(a!) decoder, Tensor packet) -> int" );
8383 m.def (" _blocks_packet_decoder_send_eof(Tensor(a!) decoder) -> int" );
8484 m.def (
85- " _blocks_packet_decoder_receive_frame(Tensor(a!) decoder) -> (Tensor, int, float, float)" );
85+ " _blocks_packet_decoder_receive_frame(Tensor(a!) decoder) -> (Tensor, int, float, float, bool)" );
86+ m.def (" _blocks_packet_decoder_bit_depth(Tensor decoder) -> int" );
8687 m.def (
8788 " _blocks_create_color_converter(str device=\" cpu\" , str device_variant=\" default\" ) -> Tensor" );
8889 m.def (" _blocks_convert_frame(Tensor(a!) converter, Tensor frame) -> Tensor" );
90+ m.def (
91+ " _blocks_frame_to_planes(Tensor frame, str device) -> (Tensor, Tensor, Tensor, Tensor, str)" );
8992 m.def (" _get_key_frame_indices(Tensor(a!) decoder) -> Tensor" );
9093 m.def (" get_json_metadata(Tensor(a!) decoder) -> str" );
9194 m.def (" get_container_json_metadata(Tensor(a!) decoder) -> str" );
@@ -861,18 +864,33 @@ torch::stable::Tensor _blocks_create_packet_decoder(
861864 torch::stable::Tensor& demuxer,
862865 std::optional<int64_t > num_threads,
863866 std::string device,
864- std::string device_variant) {
867+ std::string device_variant,
868+ std::string output_dtype) {
865869 Demuxer* demuxer_ptr = unwrap_tensor_to_pointer<Demuxer>(demuxer);
866870 validate_device_interface (device, device_variant);
867871 std::optional<int > thread_count;
868872 if (num_threads.has_value ()) {
869873 thread_count = static_cast <int >(num_threads.value ());
870874 }
875+ STD_TORCH_CHECK (
876+ output_dtype == " uint8" || output_dtype == " float32" ,
877+ " Invalid output_dtype=" ,
878+ output_dtype,
879+ " . Supported values are 'uint8' and 'float32'." );
871880 auto decoder = std::make_unique<PacketDecoder>(
872- *demuxer_ptr, StableDevice (device), device_variant, thread_count);
881+ *demuxer_ptr,
882+ StableDevice (device),
883+ device_variant,
884+ thread_count,
885+ output_dtype == " float32" ? OutputDtype::FLOAT32 : OutputDtype::UINT8 );
873886 return wrap_pointer_to_tensor<PacketDecoder>(std::move (decoder));
874887}
875888
889+ int64_t _blocks_packet_decoder_bit_depth (torch::stable::Tensor& decoder) {
890+ PacketDecoder* decoder_ptr = unwrap_tensor_to_pointer<PacketDecoder>(decoder);
891+ return static_cast <int64_t >(decoder_ptr->bit_depth ());
892+ }
893+
876894int64_t _blocks_packet_decoder_send_packet (
877895 torch::stable::Tensor& decoder,
878896 torch::stable::Tensor& packet) {
@@ -886,13 +904,15 @@ int64_t _blocks_packet_decoder_send_eof(torch::stable::Tensor& decoder) {
886904 return static_cast <int64_t >(decoder_ptr->send_eof ());
887905}
888906
889- // (frame_handle, status, pts_seconds, duration_seconds). status == 0 means a
890- // frame was produced; nonzero (EAGAIN/EOF) means no frame (dummy handle,
891- // zeros). pts/duration are stamped here (the decoder knows the stream time
892- // base) so the ColorConverter doesn't need to be bound to a stream. Native
893- // scalars avoid per-frame .item() overhead.
907+ // (frame_handle, status, pts_seconds, duration_seconds, is_device_frame).
908+ // status == 0 means a frame was produced; nonzero (EAGAIN/EOF) means no frame
909+ // (dummy handle, zeros). pts/duration are stamped here (the decoder knows the
910+ // stream time base) so the ColorConverter doesn't need to be bound to a stream.
911+ // is_device_frame says whether the samples are in device or host memory, which
912+ // isn't implied by the decoder's device: a CUDA decoder yields host frames for
913+ // streams NVDEC can't handle. Native scalars avoid per-frame .item() overhead.
894914using OpsReceiveFrameOutput =
895- std::tuple<torch::stable::Tensor, int64_t , double , double >;
915+ std::tuple<torch::stable::Tensor, int64_t , double , double , bool >;
896916
897917OpsReceiveFrameOutput _blocks_packet_decoder_receive_frame (
898918 torch::stable::Tensor& decoder) {
@@ -905,17 +925,20 @@ OpsReceiveFrameOutput _blocks_packet_decoder_receive_frame(
905925 torch::stable::full ({1 }, 0 , kStableInt64 ),
906926 static_cast <int64_t >(status),
907927 0.0 ,
908- 0.0 );
928+ 0.0 ,
929+ false );
909930 }
910931 AVRational time_base = decoder_ptr->time_base ();
911932 double pts_seconds = pts_to_seconds (get_pts_or_dts (av_frame), time_base);
912933 double duration_seconds = pts_to_seconds (get_duration (av_frame), time_base);
934+ bool is_device_frame = decoder_ptr->is_device_frame (av_frame);
913935 AVFrame* raw_frame = av_frame.release ();
914936 return std::make_tuple (
915937 wrap_frame_pointer_to_tensor (raw_frame),
916938 static_cast <int64_t >(0 ),
917939 pts_seconds,
918- duration_seconds);
940+ duration_seconds,
941+ is_device_frame);
919942}
920943
921944torch::stable::Tensor _blocks_create_color_converter (
@@ -927,6 +950,142 @@ torch::stable::Tensor _blocks_create_color_converter(
927950 return wrap_pointer_to_tensor<ColorConverter>(std::move (converter));
928951}
929952
953+ // Zero-copy access to a decoded frame's own samples, whatever its pixel format:
954+ // planar (yuv420p), semi-planar (nv12, p016), packed (yuyv422, bgra), RGB
955+ // (gbrp), grayscale, with or without alpha. Every layout FFmpeg describes as
956+ // whole bytes per sample is just a strided view over the frame's memory, which
957+ // is what AVPixFmtDescriptor's per-component plane/offset/step encodes.
958+ //
959+ // Returns 4 component views (unused ones are empty) plus a JSON description
960+ // naming the components that are real.
961+ using OpsPlanesOutput = std::tuple<
962+ torch::stable::Tensor,
963+ torch::stable::Tensor,
964+ torch::stable::Tensor,
965+ torch::stable::Tensor,
966+ std::string>;
967+
968+ // The name of component `index` in a frame of format `desc`.
969+ const char * component_name (const AVPixFmtDescriptor* desc, int index) {
970+ bool is_rgb = (desc->flags & AV_PIX_FMT_FLAG_RGB ) != 0 ;
971+ bool is_alpha = (desc->flags & AV_PIX_FMT_FLAG_ALPHA ) != 0 &&
972+ index == desc->nb_components - 1 ;
973+ if (is_alpha) {
974+ return " A" ;
975+ }
976+ static const char * rgb_names[] = {" R" , " G" , " B" };
977+ static const char * yuv_names[] = {" Y" , " U" , " V" };
978+ return is_rgb ? rgb_names[index] : yuv_names[index];
979+ }
980+
981+ OpsPlanesOutput _blocks_frame_to_planes (
982+ torch::stable::Tensor& frame,
983+ std::string device) {
984+ AVFrame* av_frame = unwrap_tensor_to_frame (frame);
985+ auto pix_fmt = static_cast <AVPixelFormat>(av_frame->format );
986+ const AVPixFmtDescriptor* desc = av_pix_fmt_desc_get (pix_fmt);
987+ STD_TORCH_CHECK (desc != nullptr , " Unknown pixel format on decoded frame" );
988+ const char * pix_fmt_name = av_get_pix_fmt_name (pix_fmt);
989+ std::string fmt_name = pix_fmt_name ? pix_fmt_name : " unknown" ;
990+
991+ // Sub-byte and palettised layouts can't be addressed by a tensor stride, and
992+ // there's no way to hand them over without unpacking them into a copy.
993+ STD_TORCH_CHECK (
994+ !(desc->flags & AV_PIX_FMT_FLAG_BITSTREAM ),
995+ " Cannot expose " ,
996+ fmt_name,
997+ " without a copy: its samples are not byte-aligned." );
998+ STD_TORCH_CHECK (
999+ !(desc->flags & AV_PIX_FMT_FLAG_PAL ),
1000+ " Cannot expose " ,
1001+ fmt_name,
1002+ " without a copy: it is palettised." );
1003+ STD_TORCH_CHECK (
1004+ !(desc->flags & AV_PIX_FMT_FLAG_FLOAT ),
1005+ " Cannot expose " ,
1006+ fmt_name,
1007+ " : floating point pixel formats are not supported yet." );
1008+ STD_TORCH_CHECK (
1009+ desc->nb_components >= 1 && desc->nb_components <= 4 ,
1010+ fmt_name,
1011+ " has " ,
1012+ static_cast <int >(desc->nb_components ),
1013+ " components, expected between 1 and 4." );
1014+
1015+ StableDevice tensor_device (device);
1016+ std::vector<torch::stable::Tensor> views;
1017+ std::stringstream component_names;
1018+
1019+ for (int c = 0 ; c < desc->nb_components ; ++c) {
1020+ const AVComponentDescriptor& comp = desc->comp [c];
1021+ STD_TORCH_CHECK (
1022+ comp.shift == 0 && comp.depth <= 16 ,
1023+ " Cannot expose component " ,
1024+ c,
1025+ " of " ,
1026+ fmt_name,
1027+ " without a copy: its samples don't start on a byte boundary." );
1028+
1029+ int64_t bytes_per_sample = (comp.depth > 8 ) ? 2 : 1 ;
1030+ int64_t linesize = av_frame->linesize [comp.plane ];
1031+ STD_TORCH_CHECK (
1032+ linesize > 0 ,
1033+ " Cannot expose " ,
1034+ fmt_name,
1035+ " : the frame has a negative line size (it is stored bottom-up), "
1036+ " which a tensor stride cannot express." );
1037+ STD_TORCH_CHECK (
1038+ comp.step % bytes_per_sample == 0 && linesize % bytes_per_sample == 0 ,
1039+ " Cannot expose component " ,
1040+ c,
1041+ " of " ,
1042+ fmt_name,
1043+ " : its byte layout isn't a whole number of samples." );
1044+
1045+ // Only the chroma components are subsampled; luma and alpha are full size.
1046+ bool is_chroma = !(desc->flags & AV_PIX_FMT_FLAG_RGB ) && (c == 1 || c == 2 );
1047+ int64_t height = is_chroma
1048+ ? AV_CEIL_RSHIFT (av_frame->height , desc->log2_chroma_h )
1049+ : av_frame->height ;
1050+ int64_t width = is_chroma
1051+ ? AV_CEIL_RSHIFT (av_frame->width , desc->log2_chroma_w )
1052+ : av_frame->width ;
1053+
1054+ // The view keeps the frame handle alive, so the planes stay valid even if
1055+ // the caller drops the frame they came from. Copying the handle tensor is
1056+ // just a refcount bump; the AVFrame is freed when the last view goes.
1057+ auto keep_frame_alive = frame;
1058+ int64_t sizes[] = {height, width};
1059+ int64_t strides[] = {
1060+ linesize / bytes_per_sample, comp.step / bytes_per_sample};
1061+ views.push_back (torch::stable::from_blob (
1062+ av_frame->data [comp.plane ] + comp.offset ,
1063+ {sizes, 2 },
1064+ {strides, 2 },
1065+ tensor_device,
1066+ (comp.depth > 8 ) ? kStableUInt16 : kStableUInt8 ,
1067+ [keep_frame_alive](void *) {}));
1068+
1069+ component_names << (c == 0 ? " \" " : " , \" " ) << component_name (desc, c)
1070+ << " \" " ;
1071+ }
1072+
1073+ std::stringstream metadata;
1074+ metadata << " {\" pix_fmt\" : \" " << fmt_name << " \" , \" components\" : ["
1075+ << component_names.str () << " ], \" container_bit_depth\" : "
1076+ << static_cast <int >(desc->comp [0 ].depth )
1077+ << " , \" colorspace\" : " << static_cast <int >(av_frame->colorspace )
1078+ << " , \" color_range\" : " << static_cast <int >(av_frame->color_range )
1079+ << " , \" is_rgb\" : "
1080+ << ((desc->flags & AV_PIX_FMT_FLAG_RGB ) ? " true" : " false" ) << " }" ;
1081+
1082+ // Absent components come back as empty tensors; `components` above says how
1083+ // many are real.
1084+ views.resize (4 , torch::stable::empty ({0 }, kStableUInt8 ));
1085+ return std::make_tuple (
1086+ views[0 ], views[1 ], views[2 ], views[3 ], metadata.str ());
1087+ }
1088+
9301089torch::stable::Tensor _blocks_convert_frame (
9311090 torch::stable::Tensor& converter,
9321091 torch::stable::Tensor& frame) {
@@ -1503,7 +1662,11 @@ STABLE_TORCH_LIBRARY_IMPL(torchcodec_ns, CPU, m) {
15031662 m.impl (
15041663 " _blocks_packet_decoder_receive_frame" ,
15051664 TORCH_BOX (&_blocks_packet_decoder_receive_frame));
1665+ m.impl (
1666+ " _blocks_packet_decoder_bit_depth" ,
1667+ TORCH_BOX (&_blocks_packet_decoder_bit_depth));
15061668 m.impl (" _blocks_convert_frame" , TORCH_BOX (&_blocks_convert_frame));
1669+ m.impl (" _blocks_frame_to_planes" , TORCH_BOX (&_blocks_frame_to_planes));
15071670 m.impl (" _test_frame_pts_equality" , TORCH_BOX (&_test_frame_pts_equality));
15081671 m.impl (
15091672 " scan_all_streams_to_update_metadata" ,
0 commit comments