react-native-alpha-video 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +903 -2
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/android/av1/build.gradle +67 -0
- package/android/av1/consumer-rules.pro +4 -0
- package/android/av1/src/main/cpp/Av1Codec_jni.cpp +8 -0
- package/android/av1/src/main/cpp/CMakeLists.txt +34 -0
- package/android/av1/src/main/java/dev/mediacache/alphavideo/av1/Av1Codec.java +21 -0
- package/android/build.gradle +85 -0
- package/android/consumer-rules.pro +2 -0
- package/android/gradle.properties +1 -0
- package/android/publish.gradle +82 -0
- package/android/settings.gradle +5 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/CMakeLists.txt +59 -0
- package/android/src/main/java/dev/mediacache/alphavideo/AlphaVideoCodecs.java +73 -0
- package/android/src/main/java/dev/mediacache/alphavideo/AlphaVideoView.java +1966 -0
- package/android/src/main/java/dev/mediacache/alphavideo/BufferedDualStreamPlayer.java +978 -0
- package/android/src/main/java/dev/mediacache/alphavideo/Fetcher.java +37 -0
- package/android/src/main/java/dev/mediacache/alphavideo/HttpFetcher.java +156 -0
- package/android/src/main/java/dev/mediacache/alphavideo/MediaCodecColor.java +47 -0
- package/android/src/main/java/dev/mediacache/alphavideo/MediaCodecDecoders.java +135 -0
- package/android/src/main/java/dev/mediacache/alphavideo/RangeSourceLink.java +54 -0
- package/android/src/main/java/dev/mediacache/alphavideo/SourceReader.java +174 -0
- package/android/src/main/java/dev/mediacache/alphavideo/SurfaceTextureVideoDecoder.java +802 -0
- package/android/src/main/java/dev/mediacache/alphavideo/WebMAlphaPlayerNative.java +95 -0
- package/android/src/main/java/dev/mediacache/alphavideo/WebMFrame.java +54 -0
- package/android/src/main/java/dev/mediacache/alphavideo/WebMSource.java +160 -0
- package/android/src/rangesource/java/dev/mediacache/alphavideo/AppDataSource.java +50 -0
- package/android/vpx/build.gradle +67 -0
- package/android/vpx/consumer-rules.pro +4 -0
- package/android/vpx/src/main/cpp/CMakeLists.txt +34 -0
- package/android/vpx/src/main/cpp/VpxCodec_jni.cpp +8 -0
- package/android/vpx/src/main/java/dev/mediacache/alphavideo/vpx/VpxCodec.java +21 -0
- package/codecs/av1/Dav1dDecoderPlugin.cpp +157 -0
- package/codecs/av1/Dav1dDecoderPlugin.h +17 -0
- package/codecs/vpx/VpxDecoderPlugin.cpp +103 -0
- package/codecs/vpx/VpxDecoderPlugin.h +17 -0
- package/cpp/AlphaDemuxer.cpp +984 -0
- package/cpp/AlphaDemuxer.h +123 -0
- package/cpp/AlphaVideoDecoderPlugin.h +84 -0
- package/cpp/Av1SequenceHeader.cpp +175 -0
- package/cpp/Av1SequenceHeader.h +25 -0
- package/cpp/ByteSource.cpp +116 -0
- package/cpp/ByteSource.h +59 -0
- package/cpp/RangeSourceClient.h +40 -0
- package/cpp/VideoDecoder.cpp +157 -0
- package/cpp/VideoDecoder.h +83 -0
- package/cpp/WebMAlphaPlayer.cpp +1666 -0
- package/cpp/WebMAlphaPlayer.h +389 -0
- package/cpp/WebMAlphaPlayer_jni.cpp +1380 -0
- package/cpp/YuvToRgb.cpp +39 -0
- package/cpp/YuvToRgb.h +40 -0
- package/ios/Sources/AlphaVideo/AlphaVideoCodecs.mm +63 -0
- package/ios/Sources/AlphaVideo/AlphaVideoInternal.h +70 -0
- package/ios/Sources/AlphaVideo/AlphaVideoPlayer.mm +994 -0
- package/ios/Sources/AlphaVideo/AlphaVideoSource.mm +476 -0
- package/ios/Sources/AlphaVideo/AlphaVideoView.mm +374 -0
- package/ios/Sources/AlphaVideo/include/AlphaVideo.h +303 -0
- package/ios/Sources/AlphaVideoAV1/AlphaVideoAV1Codec.mm +16 -0
- package/ios/Sources/AlphaVideoAV1/include/AlphaVideoAV1.h +18 -0
- package/ios/Sources/AlphaVideoVPX/AlphaVideoVPXCodec.mm +16 -0
- package/ios/Sources/AlphaVideoVPX/include/AlphaVideoVPX.h +17 -0
- package/lib/commonjs/AlphaVideoView.js +187 -0
- package/lib/commonjs/AlphaVideoView.js.map +1 -0
- package/lib/commonjs/AlphaVideoViewNativeComponent.js +47 -0
- package/lib/commonjs/AlphaVideoViewNativeComponent.js.map +1 -0
- package/lib/commonjs/AlphaVideoViewNativeComponent.web.js +11 -0
- package/lib/commonjs/AlphaVideoViewNativeComponent.web.js.map +1 -0
- package/lib/commonjs/NativeAlphaVideoModule.js +10 -0
- package/lib/commonjs/NativeAlphaVideoModule.js.map +1 -0
- package/lib/commonjs/NativeAlphaVideoModule.web.js +9 -0
- package/lib/commonjs/NativeAlphaVideoModule.web.js.map +1 -0
- package/lib/commonjs/WebPlayer.js +12 -0
- package/lib/commonjs/WebPlayer.js.map +1 -0
- package/lib/commonjs/WebPlayer.web.js +92 -0
- package/lib/commonjs/WebPlayer.web.js.map +1 -0
- package/lib/commonjs/index.js +124 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/module/AlphaVideoView.js +187 -0
- package/lib/module/AlphaVideoView.js.map +1 -0
- package/lib/module/AlphaVideoViewNativeComponent.js +47 -0
- package/lib/module/AlphaVideoViewNativeComponent.js.map +1 -0
- package/lib/module/AlphaVideoViewNativeComponent.web.js +11 -0
- package/lib/module/AlphaVideoViewNativeComponent.web.js.map +1 -0
- package/lib/module/NativeAlphaVideoModule.js +10 -0
- package/lib/module/NativeAlphaVideoModule.js.map +1 -0
- package/lib/module/NativeAlphaVideoModule.web.js +9 -0
- package/lib/module/NativeAlphaVideoModule.web.js.map +1 -0
- package/lib/module/WebPlayer.js +12 -0
- package/lib/module/WebPlayer.js.map +1 -0
- package/lib/module/WebPlayer.web.js +92 -0
- package/lib/module/WebPlayer.web.js.map +1 -0
- package/lib/module/index.js +124 -0
- package/lib/module/index.js.map +1 -0
- package/lib/typescript/react-native/src/AlphaVideoView.d.ts +189 -0
- package/lib/typescript/react-native/src/AlphaVideoView.d.ts.map +1 -0
- package/lib/typescript/react-native/src/AlphaVideoViewNativeComponent.d.ts +109 -0
- package/lib/typescript/react-native/src/AlphaVideoViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/react-native/src/AlphaVideoViewNativeComponent.web.d.ts +5 -0
- package/lib/typescript/react-native/src/AlphaVideoViewNativeComponent.web.d.ts.map +1 -0
- package/lib/typescript/react-native/src/NativeAlphaVideoModule.d.ts +21 -0
- package/lib/typescript/react-native/src/NativeAlphaVideoModule.d.ts.map +1 -0
- package/lib/typescript/react-native/src/NativeAlphaVideoModule.web.d.ts +3 -0
- package/lib/typescript/react-native/src/NativeAlphaVideoModule.web.d.ts.map +1 -0
- package/lib/typescript/react-native/src/WebPlayer.d.ts +3 -0
- package/lib/typescript/react-native/src/WebPlayer.d.ts.map +1 -0
- package/lib/typescript/react-native/src/WebPlayer.web.d.ts +9 -0
- package/lib/typescript/react-native/src/WebPlayer.web.d.ts.map +1 -0
- package/lib/typescript/react-native/src/index.d.ts +77 -0
- package/lib/typescript/react-native/src/index.d.ts.map +1 -0
- package/licenses/dav1d-COPYING.txt +23 -0
- package/licenses/libvpx-LICENSE.txt +31 -0
- package/licenses/libvpx-PATENTS.txt +23 -0
- package/licenses/libwebm-LICENSE.txt +30 -0
- package/licenses/libwebm-PATENTS.txt +23 -0
- package/package.json +150 -4
- package/prebuilt/android/arm64-v8a/lib/libdav1d.a +0 -0
- package/prebuilt/android/arm64-v8a/lib/libvpx.a +0 -0
- package/prebuilt/android/armeabi-v7a/lib/libdav1d.a +0 -0
- package/prebuilt/android/armeabi-v7a/lib/libvpx.a +0 -0
- package/prebuilt/android/x86_64/lib/libdav1d.a +0 -0
- package/prebuilt/android/x86_64/lib/libvpx.a +0 -0
- package/prebuilt/include/dav1d/common.h +94 -0
- package/prebuilt/include/dav1d/data.h +117 -0
- package/prebuilt/include/dav1d/dav1d.h +329 -0
- package/prebuilt/include/dav1d/headers.h +440 -0
- package/prebuilt/include/dav1d/picture.h +157 -0
- package/prebuilt/include/dav1d/version.h +50 -0
- package/prebuilt/include/vpx/vp8.h +136 -0
- package/prebuilt/include/vpx/vp8dx.h +228 -0
- package/prebuilt/include/vpx/vpx_codec.h +475 -0
- package/prebuilt/include/vpx/vpx_decoder.h +365 -0
- package/prebuilt/include/vpx/vpx_frame_buffer.h +83 -0
- package/prebuilt/include/vpx/vpx_image.h +209 -0
- package/prebuilt/include/vpx/vpx_integer.h +40 -0
- package/prebuilt/ios/lib/libdav1d.a +0 -0
- package/prebuilt/ios/lib/libvpx.a +0 -0
- package/prebuilt/ios-simulator/include/dav1d/common.h +94 -0
- package/prebuilt/ios-simulator/include/dav1d/data.h +117 -0
- package/prebuilt/ios-simulator/include/dav1d/dav1d.h +329 -0
- package/prebuilt/ios-simulator/include/dav1d/headers.h +440 -0
- package/prebuilt/ios-simulator/include/dav1d/picture.h +157 -0
- package/prebuilt/ios-simulator/include/dav1d/version.h +50 -0
- package/prebuilt/ios-simulator/include/vpx/vp8.h +136 -0
- package/prebuilt/ios-simulator/include/vpx/vp8dx.h +228 -0
- package/prebuilt/ios-simulator/include/vpx/vpx_codec.h +475 -0
- package/prebuilt/ios-simulator/include/vpx/vpx_decoder.h +365 -0
- package/prebuilt/ios-simulator/include/vpx/vpx_frame_buffer.h +83 -0
- package/prebuilt/ios-simulator/include/vpx/vpx_image.h +209 -0
- package/prebuilt/ios-simulator/include/vpx/vpx_integer.h +40 -0
- package/prebuilt/ios-simulator/lib/libdav1d.a +0 -0
- package/prebuilt/ios-simulator/lib/libvpx.a +0 -0
- package/react-native/android/CMakeLists.txt +14 -0
- package/react-native/android/build.gradle +110 -0
- package/react-native/android/src/main/AndroidManifest.xml +1 -0
- package/react-native/android/src/main/java/dev/mediacache/alphavideo/reactnative/AlphaVideoModule.java +102 -0
- package/react-native/android/src/main/java/dev/mediacache/alphavideo/reactnative/AlphaVideoPackage.java +50 -0
- package/react-native/android/src/main/java/dev/mediacache/alphavideo/reactnative/AlphaVideoViewManager.java +317 -0
- package/react-native/android/src/newarch/java/dev/mediacache/alphavideo/reactnative/AlphaVideoModuleSpec.java +10 -0
- package/react-native/android/src/newarch/java/dev/mediacache/alphavideo/reactnative/AlphaVideoViewManagerSpec.java +23 -0
- package/react-native/android/src/norangesource/java/dev/mediacache/alphavideo/AppDataSource.java +15 -0
- package/react-native/android/src/oldarch/java/dev/mediacache/alphavideo/reactnative/AlphaVideoModuleSpec.java +30 -0
- package/react-native/android/src/oldarch/java/dev/mediacache/alphavideo/reactnative/AlphaVideoViewManagerSpec.java +61 -0
- package/react-native/ios/RNAlphaVideoViewComponentView.mm +173 -0
- package/react-native/ios/RNAlphaVideoViewManager.mm +147 -0
- package/react-native/src/AlphaVideoView.tsx +329 -0
- package/react-native/src/AlphaVideoViewNativeComponent.ts +132 -0
- package/react-native/src/AlphaVideoViewNativeComponent.web.ts +5 -0
- package/react-native/src/NativeAlphaVideoModule.ts +22 -0
- package/react-native/src/NativeAlphaVideoModule.web.ts +2 -0
- package/react-native/src/WebPlayer.tsx +7 -0
- package/react-native/src/WebPlayer.web.tsx +60 -0
- package/react-native/src/index.tsx +144 -0
- package/react-native-alpha-video.podspec +99 -0
- package/react-native.config.js +12 -0
- package/scripts/build_codecs_android.sh +246 -0
- package/scripts/build_codecs_ios.sh +31 -0
- package/scripts/build_codecs_ios_device.sh +210 -0
- package/scripts/build_codecs_ios_simulator.sh +256 -0
- package/scripts/build_wasm.sh +230 -0
- package/scripts/build_xcframework.sh +49 -0
- package/scripts/fetch-prebuilt.sh +48 -0
- package/scripts/package_release.sh +107 -0
- package/scripts/publish-android.sh +85 -0
- package/scripts/vendor_range_source.sh +47 -0
- package/third_party/libwebm/AUTHORS.TXT +5 -0
- package/third_party/libwebm/LICENSE.TXT +30 -0
- package/third_party/libwebm/PATENTS.TXT +23 -0
- package/third_party/libwebm/README.md +7 -0
- package/third_party/libwebm/webm_parser/include/webm/buffer_reader.h +138 -0
- package/third_party/libwebm/webm_parser/include/webm/callback.h +363 -0
- package/third_party/libwebm/webm_parser/include/webm/dom_types.h +1781 -0
- package/third_party/libwebm/webm_parser/include/webm/element.h +208 -0
- package/third_party/libwebm/webm_parser/include/webm/file_reader.h +103 -0
- package/third_party/libwebm/webm_parser/include/webm/id.h +1085 -0
- package/third_party/libwebm/webm_parser/include/webm/istream_reader.h +99 -0
- package/third_party/libwebm/webm_parser/include/webm/reader.h +94 -0
- package/third_party/libwebm/webm_parser/include/webm/status.h +166 -0
- package/third_party/libwebm/webm_parser/include/webm/webm_parser.h +133 -0
- package/third_party/libwebm/webm_parser/src/ancestory.cc +334 -0
- package/third_party/libwebm/webm_parser/src/ancestory.h +83 -0
- package/third_party/libwebm/webm_parser/src/audio_parser.h +84 -0
- package/third_party/libwebm/webm_parser/src/bit_utils.cc +26 -0
- package/third_party/libwebm/webm_parser/src/bit_utils.h +24 -0
- package/third_party/libwebm/webm_parser/src/block_additions_parser.h +30 -0
- package/third_party/libwebm/webm_parser/src/block_group_parser.h +71 -0
- package/third_party/libwebm/webm_parser/src/block_header_parser.cc +76 -0
- package/third_party/libwebm/webm_parser/src/block_header_parser.h +65 -0
- package/third_party/libwebm/webm_parser/src/block_more_parser.h +32 -0
- package/third_party/libwebm/webm_parser/src/block_parser.cc +285 -0
- package/third_party/libwebm/webm_parser/src/block_parser.h +126 -0
- package/third_party/libwebm/webm_parser/src/bool_parser.h +95 -0
- package/third_party/libwebm/webm_parser/src/buffer_reader.cc +110 -0
- package/third_party/libwebm/webm_parser/src/byte_parser.h +144 -0
- package/third_party/libwebm/webm_parser/src/callback.cc +155 -0
- package/third_party/libwebm/webm_parser/src/chapter_atom_parser.h +42 -0
- package/third_party/libwebm/webm_parser/src/chapter_display_parser.h +34 -0
- package/third_party/libwebm/webm_parser/src/chapters_parser.h +28 -0
- package/third_party/libwebm/webm_parser/src/cluster_parser.h +48 -0
- package/third_party/libwebm/webm_parser/src/colour_parser.h +55 -0
- package/third_party/libwebm/webm_parser/src/content_enc_aes_settings_parser.h +32 -0
- package/third_party/libwebm/webm_parser/src/content_encoding_parser.h +38 -0
- package/third_party/libwebm/webm_parser/src/content_encodings_parser.h +30 -0
- package/third_party/libwebm/webm_parser/src/content_encryption_parser.h +37 -0
- package/third_party/libwebm/webm_parser/src/cue_point_parser.h +38 -0
- package/third_party/libwebm/webm_parser/src/cue_track_positions_parser.h +39 -0
- package/third_party/libwebm/webm_parser/src/cues_parser.h +27 -0
- package/third_party/libwebm/webm_parser/src/date_parser.cc +72 -0
- package/third_party/libwebm/webm_parser/src/date_parser.h +64 -0
- package/third_party/libwebm/webm_parser/src/ebml_parser.h +48 -0
- package/third_party/libwebm/webm_parser/src/edition_entry_parser.h +35 -0
- package/third_party/libwebm/webm_parser/src/element_parser.h +102 -0
- package/third_party/libwebm/webm_parser/src/file_reader.cc +142 -0
- package/third_party/libwebm/webm_parser/src/float_parser.cc +77 -0
- package/third_party/libwebm/webm_parser/src/float_parser.h +65 -0
- package/third_party/libwebm/webm_parser/src/id_element_parser.cc +49 -0
- package/third_party/libwebm/webm_parser/src/id_element_parser.h +56 -0
- package/third_party/libwebm/webm_parser/src/id_parser.cc +75 -0
- package/third_party/libwebm/webm_parser/src/id_parser.h +45 -0
- package/third_party/libwebm/webm_parser/src/info_parser.h +44 -0
- package/third_party/libwebm/webm_parser/src/int_parser.h +121 -0
- package/third_party/libwebm/webm_parser/src/istream_reader.cc +133 -0
- package/third_party/libwebm/webm_parser/src/master_parser.cc +298 -0
- package/third_party/libwebm/webm_parser/src/master_parser.h +230 -0
- package/third_party/libwebm/webm_parser/src/master_value_parser.h +533 -0
- package/third_party/libwebm/webm_parser/src/mastering_metadata_parser.h +57 -0
- package/third_party/libwebm/webm_parser/src/parser.h +34 -0
- package/third_party/libwebm/webm_parser/src/parser_utils.cc +34 -0
- package/third_party/libwebm/webm_parser/src/parser_utils.h +64 -0
- package/third_party/libwebm/webm_parser/src/projection_parser.h +40 -0
- package/third_party/libwebm/webm_parser/src/recursive_parser.h +93 -0
- package/third_party/libwebm/webm_parser/src/seek_head_parser.h +27 -0
- package/third_party/libwebm/webm_parser/src/seek_parser.h +37 -0
- package/third_party/libwebm/webm_parser/src/segment_parser.cc +86 -0
- package/third_party/libwebm/webm_parser/src/segment_parser.h +53 -0
- package/third_party/libwebm/webm_parser/src/simple_tag_parser.h +37 -0
- package/third_party/libwebm/webm_parser/src/size_parser.cc +56 -0
- package/third_party/libwebm/webm_parser/src/size_parser.h +43 -0
- package/third_party/libwebm/webm_parser/src/skip_callback.h +63 -0
- package/third_party/libwebm/webm_parser/src/skip_parser.cc +59 -0
- package/third_party/libwebm/webm_parser/src/skip_parser.h +35 -0
- package/third_party/libwebm/webm_parser/src/slices_parser.h +30 -0
- package/third_party/libwebm/webm_parser/src/tag_parser.h +37 -0
- package/third_party/libwebm/webm_parser/src/tags_parser.h +27 -0
- package/third_party/libwebm/webm_parser/src/targets_parser.h +35 -0
- package/third_party/libwebm/webm_parser/src/time_slice_parser.h +30 -0
- package/third_party/libwebm/webm_parser/src/track_entry_parser.h +65 -0
- package/third_party/libwebm/webm_parser/src/tracks_parser.h +27 -0
- package/third_party/libwebm/webm_parser/src/unknown_parser.cc +49 -0
- package/third_party/libwebm/webm_parser/src/unknown_parser.h +38 -0
- package/third_party/libwebm/webm_parser/src/var_int_parser.cc +71 -0
- package/third_party/libwebm/webm_parser/src/var_int_parser.h +56 -0
- package/third_party/libwebm/webm_parser/src/video_parser.h +122 -0
- package/third_party/libwebm/webm_parser/src/virtual_block_parser.cc +72 -0
- package/third_party/libwebm/webm_parser/src/virtual_block_parser.h +69 -0
- package/third_party/libwebm/webm_parser/src/void_parser.cc +49 -0
- package/third_party/libwebm/webm_parser/src/void_parser.h +41 -0
- package/third_party/libwebm/webm_parser/src/webm_parser.cc +282 -0
- package/third_party/range-source/LICENSE +21 -0
- package/third_party/range-source/README.md +8 -0
- package/third_party/range-source/include/range_source.h +395 -0
- package/third_party/range-source/include/range_source_download.h +309 -0
|
@@ -0,0 +1,1666 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebMAlphaPlayer.cpp
|
|
3
|
+
* High-performance VP9+Alpha WebM player implementation
|
|
4
|
+
*
|
|
5
|
+
* Reads WebM with AlphaDemuxer (libwebm's parser) and decodes with libvpx and dav1d directly.
|
|
6
|
+
* No dependency on platform-specific wrappers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#include "WebMAlphaPlayer.h"
|
|
10
|
+
#include "AlphaDemuxer.h"
|
|
11
|
+
#include "ByteSource.h"
|
|
12
|
+
#include "VideoDecoder.h"
|
|
13
|
+
|
|
14
|
+
#include <chrono>
|
|
15
|
+
#include <cerrno>
|
|
16
|
+
#include <cmath>
|
|
17
|
+
#include <cstdlib>
|
|
18
|
+
#include <cstring>
|
|
19
|
+
#include <algorithm>
|
|
20
|
+
#include <array>
|
|
21
|
+
#include <deque>
|
|
22
|
+
#include <map>
|
|
23
|
+
|
|
24
|
+
#ifdef __APPLE__
|
|
25
|
+
#include <stdio.h>
|
|
26
|
+
#define LOG_TAG "WebMAlphaPlayer"
|
|
27
|
+
#define LOGI(...) do { fprintf(stderr, "[%s] ", LOG_TAG); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); fflush(stderr); } while(0)
|
|
28
|
+
#define LOGE(...) do { fprintf(stderr, "[%s ERROR] ", LOG_TAG); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); fflush(stderr); } while(0)
|
|
29
|
+
#elif defined(__ANDROID__)
|
|
30
|
+
#include <android/log.h>
|
|
31
|
+
#define LOG_TAG "WebMAlphaPlayer"
|
|
32
|
+
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
|
33
|
+
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
|
34
|
+
#endif
|
|
35
|
+
|
|
36
|
+
#if !defined(LOGI)
|
|
37
|
+
#define LOGI(...) printf(__VA_ARGS__); printf("\n")
|
|
38
|
+
#endif
|
|
39
|
+
#if !defined(LOGE)
|
|
40
|
+
#define LOGE(...) fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n")
|
|
41
|
+
#endif
|
|
42
|
+
|
|
43
|
+
// Player lifecycle and stats; only when the player's verbose flag is on. Errors always log.
|
|
44
|
+
#define VLOGI(...) do { if (verbose_) LOGI(__VA_ARGS__); } while (0)
|
|
45
|
+
|
|
46
|
+
// Frame/cache debugging dumps, compiled in only with WEBM_DEBUG_LOGS
|
|
47
|
+
#if defined(WEBM_DEBUG_LOGS)
|
|
48
|
+
#define DLOGI(...) LOGI(__VA_ARGS__)
|
|
49
|
+
#else
|
|
50
|
+
#define DLOGI(...) do {} while (0)
|
|
51
|
+
#endif
|
|
52
|
+
|
|
53
|
+
namespace webm {
|
|
54
|
+
|
|
55
|
+
//==============================================================================
|
|
56
|
+
// YUVAFrame Implementation
|
|
57
|
+
//==============================================================================
|
|
58
|
+
|
|
59
|
+
YUVAFrame::YUVAFrame()
|
|
60
|
+
: yPlane(nullptr), uPlane(nullptr), vPlane(nullptr), aPlane(nullptr)
|
|
61
|
+
, ySize(0), uSize(0), vSize(0), aSize(0)
|
|
62
|
+
, width(0), height(0), pts(0), timeBase(0.0), frameIndex(0)
|
|
63
|
+
, yStride(0), uvStride(0), aStride(0)
|
|
64
|
+
{}
|
|
65
|
+
|
|
66
|
+
YUVAFrame::~YUVAFrame() {
|
|
67
|
+
std::free(yPlane);
|
|
68
|
+
std::free(uPlane);
|
|
69
|
+
std::free(vPlane);
|
|
70
|
+
std::free(aPlane);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
YUVAFrame::YUVAFrame(YUVAFrame&& other) noexcept
|
|
74
|
+
: yPlane(other.yPlane), uPlane(other.uPlane)
|
|
75
|
+
, vPlane(other.vPlane), aPlane(other.aPlane)
|
|
76
|
+
, ySize(other.ySize), uSize(other.uSize)
|
|
77
|
+
, vSize(other.vSize), aSize(other.aSize)
|
|
78
|
+
, width(other.width), height(other.height)
|
|
79
|
+
, pts(other.pts), timeBase(other.timeBase)
|
|
80
|
+
, frameIndex(other.frameIndex)
|
|
81
|
+
, yStride(other.yStride), uvStride(other.uvStride), aStride(other.aStride)
|
|
82
|
+
{
|
|
83
|
+
other.yPlane = nullptr;
|
|
84
|
+
other.uPlane = nullptr;
|
|
85
|
+
other.vPlane = nullptr;
|
|
86
|
+
other.aPlane = nullptr;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
YUVAFrame& YUVAFrame::operator=(YUVAFrame&& other) noexcept {
|
|
90
|
+
if (this != &other) {
|
|
91
|
+
// Free existing memory
|
|
92
|
+
std::free(yPlane);
|
|
93
|
+
std::free(uPlane);
|
|
94
|
+
std::free(vPlane);
|
|
95
|
+
std::free(aPlane);
|
|
96
|
+
|
|
97
|
+
// Move from other
|
|
98
|
+
yPlane = other.yPlane;
|
|
99
|
+
uPlane = other.uPlane;
|
|
100
|
+
vPlane = other.vPlane;
|
|
101
|
+
aPlane = other.aPlane;
|
|
102
|
+
ySize = other.ySize;
|
|
103
|
+
uSize = other.uSize;
|
|
104
|
+
vSize = other.vSize;
|
|
105
|
+
aSize = other.aSize;
|
|
106
|
+
width = other.width;
|
|
107
|
+
height = other.height;
|
|
108
|
+
pts = other.pts;
|
|
109
|
+
timeBase = other.timeBase;
|
|
110
|
+
frameIndex = other.frameIndex;
|
|
111
|
+
yStride = other.yStride;
|
|
112
|
+
uvStride = other.uvStride;
|
|
113
|
+
aStride = other.aStride;
|
|
114
|
+
|
|
115
|
+
other.yPlane = nullptr;
|
|
116
|
+
other.uPlane = nullptr;
|
|
117
|
+
other.vPlane = nullptr;
|
|
118
|
+
other.aPlane = nullptr;
|
|
119
|
+
}
|
|
120
|
+
return *this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
bool YUVAFrame::allocate(int w, int h) {
|
|
124
|
+
width = w;
|
|
125
|
+
height = h;
|
|
126
|
+
|
|
127
|
+
// 4:2:0 chroma is half resolution, rounded up for odd sizes
|
|
128
|
+
const int uvW = (w + 1) / 2;
|
|
129
|
+
const int uvH = (h + 1) / 2;
|
|
130
|
+
ySize = w * h;
|
|
131
|
+
uSize = uvW * uvH;
|
|
132
|
+
vSize = uvW * uvH;
|
|
133
|
+
aSize = w * h;
|
|
134
|
+
|
|
135
|
+
yStride = w;
|
|
136
|
+
uvStride = uvW;
|
|
137
|
+
aStride = w;
|
|
138
|
+
|
|
139
|
+
yPlane = static_cast<uint8_t*>(std::malloc(ySize));
|
|
140
|
+
uPlane = static_cast<uint8_t*>(std::malloc(uSize));
|
|
141
|
+
vPlane = static_cast<uint8_t*>(std::malloc(vSize));
|
|
142
|
+
aPlane = static_cast<uint8_t*>(std::malloc(aSize));
|
|
143
|
+
|
|
144
|
+
return yPlane && uPlane && vPlane && aPlane;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The planes and textures are 8-bit: deeper samples (10- and 12-bit AV1, 16 bits each) are
|
|
148
|
+
// rounded to 8 bits, which keeps the range, 64-940 in 10-bit becoming 16-235. Taking every
|
|
149
|
+
// stepX-th sample of every stepY-th row brings 4:2:2 and 4:4:4 chroma down to 4:2:0.
|
|
150
|
+
static void copyPlane(uint8_t* dst, int dstStride, const uint8_t* src, int srcStride, int w, int h, int depth,
|
|
151
|
+
int stepX = 1, int stepY = 1) {
|
|
152
|
+
if (depth <= 8 && stepX == 1) {
|
|
153
|
+
for (int y = 0; y < h; ++y) memcpy(dst + y * dstStride, src + static_cast<ptrdiff_t>(y) * stepY * srcStride, w);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const int shift = depth > 8 ? depth - 8 : 0;
|
|
157
|
+
const int half = shift ? 1 << (shift - 1) : 0;
|
|
158
|
+
for (int y = 0; y < h; ++y) {
|
|
159
|
+
const uint8_t* row = src + static_cast<ptrdiff_t>(y) * stepY * srcStride;
|
|
160
|
+
uint8_t* out = dst + y * dstStride;
|
|
161
|
+
if (depth <= 8) {
|
|
162
|
+
for (int x = 0; x < w; ++x) out[x] = row[x * stepX];
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const uint16_t* row16 = reinterpret_cast<const uint16_t*>(row);
|
|
166
|
+
for (int x = 0; x < w; ++x) out[x] = static_cast<uint8_t>(std::min((row16[x * stepX] + half) >> shift, 255));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Fills a frame's Y, U and V planes from a decoded colour picture, and its alpha opaque.
|
|
171
|
+
static void copyColour(YUVAFrame& frame, const Picture& picture) {
|
|
172
|
+
const int w = std::min(picture.width, frame.width);
|
|
173
|
+
const int h = std::min(picture.height, frame.height);
|
|
174
|
+
const int uvW = (w + 1) / 2;
|
|
175
|
+
const int uvH = (h + 1) / 2;
|
|
176
|
+
copyPlane(frame.yPlane, frame.yStride, picture.planes[0], picture.strides[0], w, h, picture.bitDepth);
|
|
177
|
+
if (picture.monochrome) {
|
|
178
|
+
for (int y = 0; y < uvH; ++y) {
|
|
179
|
+
memset(frame.uPlane + y * frame.uvStride, 128, uvW);
|
|
180
|
+
memset(frame.vPlane + y * frame.uvStride, 128, uvW);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
const int stepX = 2 >> picture.chromaShiftX;
|
|
184
|
+
const int stepY = 2 >> picture.chromaShiftY;
|
|
185
|
+
copyPlane(frame.uPlane, frame.uvStride, picture.planes[1], picture.strides[1], uvW, uvH, picture.bitDepth, stepX, stepY);
|
|
186
|
+
copyPlane(frame.vPlane, frame.uvStride, picture.planes[2], picture.strides[2], uvW, uvH, picture.bitDepth, stepX, stepY);
|
|
187
|
+
}
|
|
188
|
+
memset(frame.aPlane, 255, frame.aSize);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
static std::shared_ptr<YUVAFrame> frameFromPicture(const Picture& picture, int index) {
|
|
192
|
+
auto frame = std::make_shared<YUVAFrame>();
|
|
193
|
+
if (!frame->allocate(picture.width, picture.height)) {
|
|
194
|
+
LOGE("Failed to allocate frame buffer for %dx%d at frame %d", picture.width, picture.height, index);
|
|
195
|
+
return nullptr;
|
|
196
|
+
}
|
|
197
|
+
frame->frameIndex = index;
|
|
198
|
+
frame->pts = picture.ptsUs;
|
|
199
|
+
frame->timeBase = 1e-6;
|
|
200
|
+
copyColour(*frame, picture);
|
|
201
|
+
return frame;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// An alpha frame, 8-bit and 0-255, kept until the colour frame with its timestamp is decoded.
|
|
205
|
+
// The decoder's picture is overwritten by its next frame, so the plane is copied out.
|
|
206
|
+
struct AlphaPlane {
|
|
207
|
+
int64_t ptsUs = 0;
|
|
208
|
+
int width = 0;
|
|
209
|
+
int height = 0;
|
|
210
|
+
std::vector<uint8_t> samples; // Empty for a block without alpha, which shows opaque
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// A YUV alpha stream in limited range holds transparent as 16 and opaque as 235, so it is
|
|
214
|
+
// expanded to 0-255. An RGB one (alpha in G, plane 0), a full-range one and BlockAdditional alpha
|
|
215
|
+
// (`asIs`) are 0-255 already.
|
|
216
|
+
static AlphaPlane alphaPlaneOf(const Picture& picture, bool asIs) {
|
|
217
|
+
AlphaPlane alpha;
|
|
218
|
+
alpha.ptsUs = picture.ptsUs;
|
|
219
|
+
alpha.width = picture.width;
|
|
220
|
+
alpha.height = picture.height;
|
|
221
|
+
alpha.samples.resize(static_cast<size_t>(picture.width) * picture.height);
|
|
222
|
+
copyPlane(alpha.samples.data(), picture.width, picture.planes[0], picture.strides[0], picture.width,
|
|
223
|
+
picture.height, picture.bitDepth);
|
|
224
|
+
if (asIs || picture.rgb || picture.colorRange == kRangeFull) return alpha;
|
|
225
|
+
static const auto expand = [] {
|
|
226
|
+
std::array<uint8_t, 256> lut{};
|
|
227
|
+
for (int v = 0; v < 256; v++) {
|
|
228
|
+
lut[v] = static_cast<uint8_t>(std::clamp((v - 16) * 255 / 219.0 + 0.5, 0.0, 255.0));
|
|
229
|
+
}
|
|
230
|
+
return lut;
|
|
231
|
+
}();
|
|
232
|
+
for (uint8_t& v : alpha.samples) v = expand[v];
|
|
233
|
+
return alpha;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
static void copyAlpha(YUVAFrame& frame, const AlphaPlane& alpha) {
|
|
237
|
+
if (alpha.samples.empty()) return; // Opaque, as copyColour left it
|
|
238
|
+
const int w = std::min(alpha.width, frame.width);
|
|
239
|
+
const int h = std::min(alpha.height, frame.height);
|
|
240
|
+
for (int y = 0; y < h; ++y) {
|
|
241
|
+
memcpy(frame.aPlane + y * frame.aStride, alpha.samples.data() + static_cast<size_t>(y) * alpha.width, w);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
double YUVAFrame::getDurationMs() const {
|
|
246
|
+
if (timeBase > 0) {
|
|
247
|
+
return (pts * timeBase) * 1000.0;
|
|
248
|
+
}
|
|
249
|
+
return 0.0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
//==============================================================================
|
|
253
|
+
// FrameCache Implementation
|
|
254
|
+
//==============================================================================
|
|
255
|
+
|
|
256
|
+
FrameCache::FrameCache(size_t maxFrames) : maxSize_(maxFrames) {
|
|
257
|
+
DLOGI("🗄️ FrameCache created with maxSize=%zu", maxSize_);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
void FrameCache::put(const std::string& key, std::shared_ptr<YUVAFrame> frame) {
|
|
261
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
262
|
+
|
|
263
|
+
auto it = lruMap_.find(key);
|
|
264
|
+
if (it != lruMap_.end()) {
|
|
265
|
+
// Key exists - update and move to front
|
|
266
|
+
cache_[key] = frame;
|
|
267
|
+
lruList_.erase(it->second);
|
|
268
|
+
lruList_.push_front(key);
|
|
269
|
+
lruMap_[key] = lruList_.begin();
|
|
270
|
+
} else {
|
|
271
|
+
// New key - insert
|
|
272
|
+
if (cache_.size() >= maxSize_) {
|
|
273
|
+
// In stream mode, the prefetch loop should have already called evictFurthestBehind
|
|
274
|
+
// But for preload mode or edge cases, use simple LRU eviction
|
|
275
|
+
evictOldest();
|
|
276
|
+
}
|
|
277
|
+
cache_[key] = frame;
|
|
278
|
+
lruList_.push_front(key);
|
|
279
|
+
lruMap_[key] = lruList_.begin();
|
|
280
|
+
|
|
281
|
+
// Log cache size periodically during preload
|
|
282
|
+
if (cache_.size() % 50 == 0) {
|
|
283
|
+
DLOGI("📊 Cache now has %zu frames (max=%zu)", cache_.size(), maxSize_);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
std::shared_ptr<YUVAFrame> FrameCache::get(const std::string& key) {
|
|
289
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
290
|
+
|
|
291
|
+
auto it = cache_.find(key);
|
|
292
|
+
if (it == cache_.end()) {
|
|
293
|
+
return nullptr;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Move to front (most recently used)
|
|
297
|
+
auto lruIt = lruMap_[key];
|
|
298
|
+
lruList_.erase(lruIt);
|
|
299
|
+
lruList_.push_front(key);
|
|
300
|
+
lruMap_[key] = lruList_.begin();
|
|
301
|
+
|
|
302
|
+
return it->second;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
void FrameCache::clear() {
|
|
306
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
307
|
+
cache_.clear();
|
|
308
|
+
lruList_.clear();
|
|
309
|
+
lruMap_.clear();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
size_t FrameCache::size() const {
|
|
313
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
314
|
+
return cache_.size();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
bool FrameCache::contains(const std::string& key) const {
|
|
318
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
319
|
+
return cache_.find(key) != cache_.end();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
void FrameCache::evict(const std::string& key) {
|
|
323
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
324
|
+
auto it = cache_.find(key);
|
|
325
|
+
if (it == cache_.end()) return;
|
|
326
|
+
|
|
327
|
+
// Remove from LRU tracking
|
|
328
|
+
auto lruIt = lruMap_.find(key);
|
|
329
|
+
if (lruIt != lruMap_.end()) {
|
|
330
|
+
lruList_.erase(lruIt->second);
|
|
331
|
+
lruMap_.erase(lruIt);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
cache_.erase(it);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
void FrameCache::setMaxSize(size_t maxFrames) {
|
|
338
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
339
|
+
maxSize_ = maxFrames;
|
|
340
|
+
|
|
341
|
+
// Evict excess frames if needed
|
|
342
|
+
while (cache_.size() > maxSize_) {
|
|
343
|
+
evictOldest();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
void FrameCache::evictOldest() {
|
|
348
|
+
if (lruList_.empty()) return;
|
|
349
|
+
|
|
350
|
+
std::string oldestKey = lruList_.back();
|
|
351
|
+
lruList_.pop_back();
|
|
352
|
+
lruMap_.erase(oldestKey);
|
|
353
|
+
cache_.erase(oldestKey);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
size_t FrameCache::maxSize() const {
|
|
357
|
+
return maxSize_;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
bool FrameCache::isFull() const {
|
|
361
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
362
|
+
return cache_.size() >= maxSize_;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
bool FrameCache::evictFurthestBehind(int playbackPos, int totalFrames, const std::string& keyPrefix) {
|
|
366
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
367
|
+
|
|
368
|
+
if (cache_.empty() || totalFrames <= 0) return false;
|
|
369
|
+
|
|
370
|
+
// Find the frame that is furthest behind playbackPos in circular sense
|
|
371
|
+
std::string furthestKey;
|
|
372
|
+
int furthestDist = 0; // Most negative distance = furthest behind
|
|
373
|
+
|
|
374
|
+
for (const auto& pair : cache_) {
|
|
375
|
+
// Extract frame number from key (format: "frame_123")
|
|
376
|
+
size_t underscorePos = pair.first.find('_');
|
|
377
|
+
if (underscorePos == std::string::npos) continue;
|
|
378
|
+
|
|
379
|
+
int frameNum = std::stoi(pair.first.substr(underscorePos + 1));
|
|
380
|
+
|
|
381
|
+
// Calculate circular distance: positive = ahead, negative = behind
|
|
382
|
+
int dist = frameNum - playbackPos;
|
|
383
|
+
// Normalize to [-totalFrames/2, totalFrames/2] for wraparound
|
|
384
|
+
if (dist > totalFrames / 2) dist -= totalFrames;
|
|
385
|
+
if (dist < -totalFrames / 2) dist += totalFrames;
|
|
386
|
+
|
|
387
|
+
// Track the frame with the most negative distance (furthest behind)
|
|
388
|
+
if (furthestKey.empty() || dist < furthestDist) {
|
|
389
|
+
furthestDist = dist;
|
|
390
|
+
furthestKey = pair.first;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!furthestKey.empty() && furthestDist < 0) { // Evict if behind playback
|
|
395
|
+
// Remove from LRU tracking
|
|
396
|
+
auto lruIt = lruMap_.find(furthestKey);
|
|
397
|
+
if (lruIt != lruMap_.end()) {
|
|
398
|
+
lruList_.erase(lruIt->second);
|
|
399
|
+
lruMap_.erase(lruIt);
|
|
400
|
+
}
|
|
401
|
+
cache_.erase(furthestKey);
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
//==============================================================================
|
|
409
|
+
// DecoderStats Implementation
|
|
410
|
+
//==============================================================================
|
|
411
|
+
|
|
412
|
+
double DecoderStats::getAverageDecodeTimeMs() const {
|
|
413
|
+
if (framesDecoded == 0) return 0.0;
|
|
414
|
+
return static_cast<double>(decodeTimeUs) / framesDecoded / 1000.0;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
double DecoderStats::getCacheHitRate() const {
|
|
418
|
+
int64_t total = cacheHits + cacheMisses;
|
|
419
|
+
if (total == 0) return 0.0;
|
|
420
|
+
return static_cast<double>(cacheHits) / total * 100.0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
void DecoderStats::reset() {
|
|
424
|
+
framesDecoded = 0;
|
|
425
|
+
framesCached = 0;
|
|
426
|
+
cacheHits = 0;
|
|
427
|
+
cacheMisses = 0;
|
|
428
|
+
decodeTimeUs = 0;
|
|
429
|
+
peakMemoryBytes = 0;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
//==============================================================================
|
|
433
|
+
// WebMAlphaPlayer Implementation
|
|
434
|
+
//==============================================================================
|
|
435
|
+
|
|
436
|
+
WebMAlphaPlayer::WebMAlphaPlayer() {
|
|
437
|
+
frameCache_ = std::make_unique<FrameCache>(config_.cacheSize);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
WebMAlphaPlayer::~WebMAlphaPlayer() {
|
|
441
|
+
unload();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
void WebMAlphaPlayer::setConfig(const PlayerConfig& config) {
|
|
445
|
+
std::lock_guard<std::mutex> lock(configMutex_);
|
|
446
|
+
VLOGI("⚙️ setConfig called: cacheSize=%d, preloadAllFrames=%d", config.cacheSize, config.preloadAllFrames);
|
|
447
|
+
|
|
448
|
+
// Check if cache size is actually changing
|
|
449
|
+
size_t oldCacheSize = config_.cacheSize;
|
|
450
|
+
config_ = config;
|
|
451
|
+
|
|
452
|
+
// The loop reads the atomic, never the struct, so the config's speed has to be mirrored
|
|
453
|
+
// across or a rate passed at construction would be silently ignored.
|
|
454
|
+
playbackSpeed_ = config.playbackSpeed > 0.01 ? config.playbackSpeed : 0.01;
|
|
455
|
+
|
|
456
|
+
// Only recreate cache if size changed AND cache is empty
|
|
457
|
+
// This preserves preloaded frames from decodeAllFrames()
|
|
458
|
+
if (frameCache_) {
|
|
459
|
+
size_t currentCacheFrames = frameCache_->size();
|
|
460
|
+
if (currentCacheFrames > 0) {
|
|
461
|
+
VLOGI("⚙️ Preserving existing cache with %zu frames (not recreating)", currentCacheFrames);
|
|
462
|
+
} else if (config_.cacheSize != oldCacheSize) {
|
|
463
|
+
VLOGI("⚙️ Recreating empty cache: %zu -> %d", oldCacheSize, config_.cacheSize);
|
|
464
|
+
frameCache_ = std::make_unique<FrameCache>(config_.cacheSize);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
PlayerConfig WebMAlphaPlayer::getConfig() const {
|
|
470
|
+
std::lock_guard<std::mutex> lock(configMutex_);
|
|
471
|
+
return config_;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
bool WebMAlphaPlayer::loadFromFile(const std::string& path) {
|
|
475
|
+
unload();
|
|
476
|
+
|
|
477
|
+
// Reset stop flag so decodeAllFrames doesn't exit immediately
|
|
478
|
+
shouldStop_ = false;
|
|
479
|
+
ioAbort_ = false;
|
|
480
|
+
|
|
481
|
+
std::unique_ptr<ByteSource> source = openFile(path);
|
|
482
|
+
if (!source) {
|
|
483
|
+
emitEvent("error", std::string("Failed to open file: ") + std::strerror(errno));
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
return finishLoad(std::move(source));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
bool WebMAlphaPlayer::loadFromFileRange(int fd, int64_t offset, int64_t length) {
|
|
490
|
+
unload();
|
|
491
|
+
|
|
492
|
+
// Reset stop flag so decodeAllFrames doesn't exit immediately
|
|
493
|
+
shouldStop_ = false;
|
|
494
|
+
ioAbort_ = false;
|
|
495
|
+
|
|
496
|
+
std::unique_ptr<ByteSource> source = openFileRange(fd, offset, length);
|
|
497
|
+
if (!source) {
|
|
498
|
+
emitEvent("error", "Failed to open file range");
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
return finishLoad(std::move(source));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
bool WebMAlphaPlayer::loadFromReader(std::shared_ptr<SourceReader> reader) {
|
|
505
|
+
unload();
|
|
506
|
+
|
|
507
|
+
shouldStop_ = false;
|
|
508
|
+
ioAbort_ = false;
|
|
509
|
+
|
|
510
|
+
if (!reader) {
|
|
511
|
+
emitEvent("error", "No source to read");
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
reader_ = reader;
|
|
515
|
+
return finishLoad(openReader(std::move(reader), &ioAbort_));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
bool WebMAlphaPlayer::finishLoad(std::unique_ptr<ByteSource> source) {
|
|
519
|
+
demuxer_ = AlphaDemuxer::open(std::move(source));
|
|
520
|
+
if (!demuxer_) {
|
|
521
|
+
std::string readError = reader_ ? reader_->lastError() : std::string();
|
|
522
|
+
emitEvent("error", readError.empty() ? "Failed to open video: not a WebM with a VP9 or AV1 track"
|
|
523
|
+
: "Reading the video failed: " + readError);
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (!openStreams()) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!initCodecs()) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
loaded_ = true;
|
|
536
|
+
emitEvent("loaded", std::to_string(totalFrames_) + " frames");
|
|
537
|
+
|
|
538
|
+
// Reset first pass flag
|
|
539
|
+
firstPassComplete_ = false;
|
|
540
|
+
|
|
541
|
+
// Handle playback modes
|
|
542
|
+
if (config_.mode == PlaybackMode::Stream) {
|
|
543
|
+
// Stream mode: sequential background decode, frames behind the playhead evicted. The
|
|
544
|
+
// cache holds the shown frame, the frames decoded after it, and the previous frame until
|
|
545
|
+
// it's evicted; anything more would be evicted by LRU, the next frame to show first.
|
|
546
|
+
int prefetchAhead = config_.streamPrefetchAhead > 0 ? config_.streamPrefetchAhead : 8;
|
|
547
|
+
frameCache_->setMaxSize(prefetchAhead + 2);
|
|
548
|
+
VLOGI("📺 Stream mode enabled: prefetchAhead=%d", prefetchAhead);
|
|
549
|
+
|
|
550
|
+
// Reset streaming decode position
|
|
551
|
+
streamDecodePos_ = 0;
|
|
552
|
+
prefetchTarget_ = 0;
|
|
553
|
+
// Frame 0 goes through the frame callback once decoded, so a clip that isn't played
|
|
554
|
+
// straight away still shows its first frame.
|
|
555
|
+
pendingSeekFrame_ = 0;
|
|
556
|
+
|
|
557
|
+
// Start background sequential decode thread immediately
|
|
558
|
+
prefetchThread_ = std::make_unique<std::thread>(&WebMAlphaPlayer::prefetchLoop, this);
|
|
559
|
+
|
|
560
|
+
// Wait briefly for initial frames to be decoded
|
|
561
|
+
auto startWait = std::chrono::high_resolution_clock::now();
|
|
562
|
+
int minFrames = std::min(5, totalFrames_);
|
|
563
|
+
while (streamDecodePos_.load() < minFrames && !shouldStop_) {
|
|
564
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
565
|
+
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
566
|
+
std::chrono::high_resolution_clock::now() - startWait);
|
|
567
|
+
if (elapsed.count() > 500) {
|
|
568
|
+
VLOGI("📺 Initial decode timeout after 500ms, continuing with %d frames",
|
|
569
|
+
streamDecodePos_.load());
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
VLOGI("📺 Initial decode ready: %d frames", streamDecodePos_.load());
|
|
574
|
+
} else if (config_.mode == PlaybackMode::StreamAndPreload) {
|
|
575
|
+
// StreamAndPreload (hybrid) mode: start playing immediately, cache ALL frames
|
|
576
|
+
// After first pass completes, loop from cache without re-decoding
|
|
577
|
+
frameCache_->setMaxSize(totalFrames_ + 10); // Cache all frames
|
|
578
|
+
VLOGI("📺 StreamAndPreload mode enabled: cacheSize=%d (all frames), prefetchAhead=%d",
|
|
579
|
+
totalFrames_ + 10, config_.streamPrefetchAhead);
|
|
580
|
+
|
|
581
|
+
// Reset streaming decode position
|
|
582
|
+
streamDecodePos_ = 0;
|
|
583
|
+
prefetchTarget_ = 0;
|
|
584
|
+
pendingSeekFrame_ = 0; // as in Stream mode: frame 0 is shown without play()
|
|
585
|
+
|
|
586
|
+
// Start background sequential decode thread - will decode ALL frames
|
|
587
|
+
prefetchThread_ = std::make_unique<std::thread>(&WebMAlphaPlayer::prefetchLoop, this);
|
|
588
|
+
|
|
589
|
+
// DON'T wait for initial frames here - return immediately to avoid blocking
|
|
590
|
+
// the global decode queue when multiple players are spawned simultaneously.
|
|
591
|
+
// The playback loop will handle frame-not-ready gracefully by waiting/retrying.
|
|
592
|
+
VLOGI("📺 StreamAndPreload started (non-blocking), decode thread running in background");
|
|
593
|
+
} else {
|
|
594
|
+
// Preload mode: decode all frames upfront before playback
|
|
595
|
+
decodeAllFrames();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
bool WebMAlphaPlayer::loadFromMemory(const uint8_t* data, size_t size) {
|
|
602
|
+
unload();
|
|
603
|
+
|
|
604
|
+
// Reset stop flag so decodeAllFrames doesn't exit immediately
|
|
605
|
+
shouldStop_ = false;
|
|
606
|
+
ioAbort_ = false;
|
|
607
|
+
|
|
608
|
+
// A copy: the caller's buffer may go away
|
|
609
|
+
return finishLoad(openMemory(data, size));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
void WebMAlphaPlayer::unload() {
|
|
613
|
+
// Protect against concurrent unload calls (e.g., during view detach/reattach)
|
|
614
|
+
std::lock_guard<std::mutex> unloadLock(unloadMutex_);
|
|
615
|
+
|
|
616
|
+
ioAbort_ = true;
|
|
617
|
+
stop();
|
|
618
|
+
|
|
619
|
+
// Signal prefetch thread to stop
|
|
620
|
+
prefetchCV_.notify_all();
|
|
621
|
+
|
|
622
|
+
// Wait for any in-progress decode operations (including synchronous decodeAllFrames)
|
|
623
|
+
// Use a timeout to prevent ANR - if decode is stuck, shouldStop_ flag should
|
|
624
|
+
// eventually cause it to exit. We proceed with cleanup after timeout.
|
|
625
|
+
{
|
|
626
|
+
// Try to acquire lock with 500ms timeout - enough for normal operations,
|
|
627
|
+
// but short enough to not cause ANR (5 second threshold)
|
|
628
|
+
if (!decodeMutex_.try_lock_for(std::chrono::milliseconds(500))) {
|
|
629
|
+
VLOGI("unload: decodeMutex_ acquisition timed out after 500ms - proceeding anyway");
|
|
630
|
+
// shouldStop_ is already set, decodeAllFrames will exit when it checks
|
|
631
|
+
} else {
|
|
632
|
+
// Lock acquired normally - immediately release since we just needed to wait
|
|
633
|
+
decodeMutex_.unlock();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Wait for threads to finish - must check both pointer and joinable state
|
|
638
|
+
// to avoid crash when unload is called multiple times
|
|
639
|
+
if (decodeThread_) {
|
|
640
|
+
if (decodeThread_->joinable()) {
|
|
641
|
+
decodeThread_->join();
|
|
642
|
+
}
|
|
643
|
+
decodeThread_.reset();
|
|
644
|
+
}
|
|
645
|
+
if (playbackThread_) {
|
|
646
|
+
if (playbackThread_->joinable()) {
|
|
647
|
+
playbackThread_->join();
|
|
648
|
+
}
|
|
649
|
+
playbackThread_.reset();
|
|
650
|
+
}
|
|
651
|
+
if (prefetchThread_) {
|
|
652
|
+
if (prefetchThread_->joinable()) {
|
|
653
|
+
prefetchThread_->join();
|
|
654
|
+
}
|
|
655
|
+
prefetchThread_.reset();
|
|
656
|
+
}
|
|
657
|
+
prefetchTarget_ = -1;
|
|
658
|
+
|
|
659
|
+
closeCodecs();
|
|
660
|
+
demuxer_.reset();
|
|
661
|
+
reader_.reset();
|
|
662
|
+
|
|
663
|
+
if (frameCache_) {
|
|
664
|
+
frameCache_->clear();
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
width_ = height_ = totalFrames_ = 0;
|
|
668
|
+
duration_ = frameRate_ = 0.0;
|
|
669
|
+
loaded_ = false;
|
|
670
|
+
currentFrame_ = 0;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
bool WebMAlphaPlayer::openStreams() {
|
|
674
|
+
const AlphaDemuxer::Info& info = demuxer_->info();
|
|
675
|
+
|
|
676
|
+
const AlphaDemuxer::StreamInfo* tracks[] = {
|
|
677
|
+
&info.color, info.hasAlpha && !info.alphaInBlockAdditional ? &info.alpha : nullptr};
|
|
678
|
+
for (int track = 0; track < 2; track++) {
|
|
679
|
+
if (!tracks[track]) continue;
|
|
680
|
+
std::string whyNot;
|
|
681
|
+
if (!findDecoderPlugin(*tracks[track], info.width, info.height, &whyNot)) {
|
|
682
|
+
emitError(kErrorNoDecoder, std::string(track ? "The alpha" : "The colour") + " track can't play: " + whyNot);
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
VLOGI("Colour: %s profile %d, %d-bit; alpha: %s",
|
|
687
|
+
info.color.codec == AlphaDemuxer::AV1 ? "AV1"
|
|
688
|
+
: info.color.codec == AlphaDemuxer::VP8 ? "VP8"
|
|
689
|
+
: "VP9",
|
|
690
|
+
info.color.profile, info.color.bitDepth,
|
|
691
|
+
!info.hasAlpha ? "none" : info.alphaInBlockAdditional ? "BlockAdditional" : "track");
|
|
692
|
+
|
|
693
|
+
width_ = info.width;
|
|
694
|
+
height_ = info.height;
|
|
695
|
+
{
|
|
696
|
+
std::lock_guard<std::mutex> lock(yuvToRgbMutex_);
|
|
697
|
+
yuvToRgb_ = yuvToRgbFor(info.color.colorRange, info.color.colorSpace, width_, height_);
|
|
698
|
+
}
|
|
699
|
+
VLOGI("Colour: range=%d matrix=%d", info.color.colorRange, info.color.colorSpace);
|
|
700
|
+
|
|
701
|
+
frameRate_ = info.frameRate > 0 ? info.frameRate : 30.0;
|
|
702
|
+
duration_ = static_cast<double>(info.durationUs) / 1e6;
|
|
703
|
+
totalFrames_ = info.totalFrames > 0 ? info.totalFrames : 100;
|
|
704
|
+
startUs_ = info.startUs;
|
|
705
|
+
alphaAsIs_ = info.alphaInBlockAdditional;
|
|
706
|
+
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
bool WebMAlphaPlayer::initCodecs() {
|
|
711
|
+
const AlphaDemuxer::Info& info = demuxer_->info();
|
|
712
|
+
std::string error;
|
|
713
|
+
colorDecoder_ = VideoDecoder::create(info.color, info.width, info.height, config_.decodeThreads, &error);
|
|
714
|
+
if (!colorDecoder_) {
|
|
715
|
+
emitEvent("error", "Failed to open the colour decoder (" + error + ")");
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
std::string name = colorDecoder_->name();
|
|
719
|
+
if (info.hasAlpha) {
|
|
720
|
+
alphaDecoder_ = VideoDecoder::create(info.alpha, info.width, info.height, config_.decodeThreads, &error);
|
|
721
|
+
if (!alphaDecoder_) {
|
|
722
|
+
emitEvent("error", "Failed to open the alpha decoder (" + error + ")");
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
if (name != alphaDecoder_->name()) name += std::string(", ") + alphaDecoder_->name();
|
|
726
|
+
}
|
|
727
|
+
VLOGI("Decoders: %s, %d threads each", name.c_str(), config_.decodeThreads);
|
|
728
|
+
std::lock_guard<std::mutex> lock(decoderNameMutex_);
|
|
729
|
+
decoderName_ = name;
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
std::string WebMAlphaPlayer::getDecoderName() const {
|
|
734
|
+
std::lock_guard<std::mutex> lock(decoderNameMutex_);
|
|
735
|
+
return decoderName_;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
void WebMAlphaPlayer::closeCodecs() {
|
|
739
|
+
colorDecoder_.reset();
|
|
740
|
+
alphaDecoder_.reset();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
std::shared_ptr<YUVAFrame> WebMAlphaPlayer::decodeFrame(int index) {
|
|
744
|
+
// Check cache first (with null safety)
|
|
745
|
+
if (!frameCache_) {
|
|
746
|
+
return nullptr;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
std::string cacheKey = makeFrameKey(index);
|
|
750
|
+
auto cachedFrame = frameCache_->get(cacheKey);
|
|
751
|
+
if (cachedFrame) {
|
|
752
|
+
stats_.cacheHits++;
|
|
753
|
+
return cachedFrame;
|
|
754
|
+
}
|
|
755
|
+
stats_.cacheMisses++;
|
|
756
|
+
|
|
757
|
+
// Acquire decode mutex - the demuxer and decoders are NOT thread-safe
|
|
758
|
+
// This prevents concurrent access during preload (decodeAllFrames) or other decodes
|
|
759
|
+
std::lock_guard<std::timed_mutex> decodeLock(decodeMutex_);
|
|
760
|
+
|
|
761
|
+
// Double-check cache after acquiring lock - another thread may have decoded this frame
|
|
762
|
+
cachedFrame = frameCache_->get(cacheKey);
|
|
763
|
+
if (cachedFrame) {
|
|
764
|
+
stats_.cacheHits++;
|
|
765
|
+
return cachedFrame;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
auto startTime = std::chrono::high_resolution_clock::now();
|
|
769
|
+
|
|
770
|
+
// Decode from the keyframe before the frame, matching frames by timestamp: the demuxer lands
|
|
771
|
+
// on that keyframe, and decoders with frame delay (dav1d) return frames late.
|
|
772
|
+
demuxer_->seekToTimeUs(startUs_ + static_cast<int64_t>(std::llround(index / frameRate_ * 1e6)));
|
|
773
|
+
colorDecoder_->flush();
|
|
774
|
+
if (alphaDecoder_) {
|
|
775
|
+
alphaDecoder_->flush();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
std::shared_ptr<YUVAFrame> result;
|
|
779
|
+
AlphaPlane alphaForIndex;
|
|
780
|
+
bool haveAlpha = false;
|
|
781
|
+
Picture picture;
|
|
782
|
+
|
|
783
|
+
auto reachesIndex = [&](int64_t ptsUs) { return frameIndexForPts(ptsUs) >= index; };
|
|
784
|
+
auto takeColour = [&] {
|
|
785
|
+
while (!result && colorDecoder_->receive(&picture)) {
|
|
786
|
+
if (!reachesIndex(picture.ptsUs)) continue;
|
|
787
|
+
result = frameFromPicture(picture, index);
|
|
788
|
+
if (!result) return false;
|
|
789
|
+
}
|
|
790
|
+
return true;
|
|
791
|
+
};
|
|
792
|
+
auto takeAlpha = [&] {
|
|
793
|
+
while (!haveAlpha && alphaDecoder_->receive(&picture)) {
|
|
794
|
+
if (!reachesIndex(picture.ptsUs)) continue;
|
|
795
|
+
alphaForIndex = alphaPlaneOf(picture, alphaAsIs_);
|
|
796
|
+
haveAlpha = true;
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
auto done = [&] { return result && (!alphaDecoder_ || haveAlpha); };
|
|
800
|
+
|
|
801
|
+
AlphaDemuxer::Packet packet;
|
|
802
|
+
bool failed = false;
|
|
803
|
+
while (!done() && !failed && !shouldStop_) {
|
|
804
|
+
if (!demuxer_->readPacket(&packet)) {
|
|
805
|
+
if (!result) {
|
|
806
|
+
colorDecoder_->drain();
|
|
807
|
+
failed = !takeColour();
|
|
808
|
+
}
|
|
809
|
+
if (alphaDecoder_ && !haveAlpha) {
|
|
810
|
+
alphaDecoder_->drain();
|
|
811
|
+
takeAlpha();
|
|
812
|
+
}
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
if (packet.stream == 0 && !result) {
|
|
816
|
+
if (colorDecoder_->send(packet.data, packet.size, packet.ptsUs)) failed = !takeColour();
|
|
817
|
+
} else if (packet.stream == 1 && alphaDecoder_ && !haveAlpha) {
|
|
818
|
+
if (packet.size == 0) {
|
|
819
|
+
// A block without alpha: its frame shows opaque
|
|
820
|
+
haveAlpha = reachesIndex(packet.ptsUs);
|
|
821
|
+
} else if (alphaDecoder_->send(packet.data, packet.size, packet.ptsUs)) {
|
|
822
|
+
takeAlpha();
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// A frame whose alpha stream ended early stays opaque, as copyColour left it
|
|
828
|
+
if (result && haveAlpha) copyAlpha(*result, alphaForIndex);
|
|
829
|
+
|
|
830
|
+
auto endTime = std::chrono::high_resolution_clock::now();
|
|
831
|
+
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(endTime - startTime);
|
|
832
|
+
stats_.decodeTimeUs += duration.count();
|
|
833
|
+
stats_.framesDecoded++;
|
|
834
|
+
|
|
835
|
+
// Cache the frame (with null safety)
|
|
836
|
+
if (result && frameCache_ && !shouldStop_) {
|
|
837
|
+
frameCache_->put(cacheKey, result);
|
|
838
|
+
stats_.framesCached++;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return result;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
void WebMAlphaPlayer::decodeAllFrames() {
|
|
845
|
+
// Hold decode mutex for the entire operation - unload() will wait for this
|
|
846
|
+
std::lock_guard<std::timed_mutex> decodeLock(decodeMutex_);
|
|
847
|
+
|
|
848
|
+
// Check if we should stop before starting (unload might have been called)
|
|
849
|
+
if (shouldStop_) {
|
|
850
|
+
VLOGI("decodeAllFrames: Aborted - shouldStop_ already set");
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
preloading_ = true;
|
|
854
|
+
|
|
855
|
+
emitEvent("preload_start", std::to_string(totalFrames_) + " frames");
|
|
856
|
+
|
|
857
|
+
// Ensure cache can hold all frames
|
|
858
|
+
if (totalFrames_ > config_.cacheSize) {
|
|
859
|
+
VLOGI("Resizing frame cache from %d to %d to fit all frames", config_.cacheSize, totalFrames_ + 10);
|
|
860
|
+
config_.cacheSize = totalFrames_ + 10;
|
|
861
|
+
frameCache_ = std::make_unique<FrameCache>(config_.cacheSize);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
auto startTime = std::chrono::high_resolution_clock::now();
|
|
865
|
+
|
|
866
|
+
// Reset to beginning
|
|
867
|
+
demuxer_->seekToStart();
|
|
868
|
+
colorDecoder_->flush();
|
|
869
|
+
if (alphaDecoder_) {
|
|
870
|
+
alphaDecoder_->flush();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
int frameIndex = 0;
|
|
874
|
+
int alphaFrameIndex = 0; // Track alpha frames separately (NOT static!)
|
|
875
|
+
|
|
876
|
+
// Two-stream files: getFrame() can hand out a cached frame while this runs, so a colour frame
|
|
877
|
+
// is cached only once the alpha frame with its index is in. Whichever decodes first waits here.
|
|
878
|
+
std::map<int, std::shared_ptr<YUVAFrame>> colourAwaitingAlpha;
|
|
879
|
+
std::map<int, AlphaPlane> alphaAwaitingColour;
|
|
880
|
+
auto cacheFrame = [&](int index, const std::shared_ptr<YUVAFrame>& frame) {
|
|
881
|
+
frameCache_->put(makeFrameKey(index), frame);
|
|
882
|
+
stats_.framesDecoded++;
|
|
883
|
+
stats_.framesCached++;
|
|
884
|
+
};
|
|
885
|
+
auto addColour = [&](int index, const std::shared_ptr<YUVAFrame>& frame) {
|
|
886
|
+
auto alpha = alphaAwaitingColour.find(index);
|
|
887
|
+
if (alpha != alphaAwaitingColour.end()) {
|
|
888
|
+
copyAlpha(*frame, alpha->second);
|
|
889
|
+
alphaAwaitingColour.erase(alpha);
|
|
890
|
+
} else if (alphaDecoder_) {
|
|
891
|
+
colourAwaitingAlpha[index] = frame;
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
cacheFrame(index, frame);
|
|
895
|
+
};
|
|
896
|
+
auto addAlpha = [&](int index, AlphaPlane alpha) {
|
|
897
|
+
auto colour = colourAwaitingAlpha.find(index);
|
|
898
|
+
if (colour == colourAwaitingAlpha.end()) {
|
|
899
|
+
alphaAwaitingColour[index] = std::move(alpha);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
copyAlpha(*colour->second, alpha);
|
|
903
|
+
cacheFrame(index, colour->second);
|
|
904
|
+
colourAwaitingAlpha.erase(colour);
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
Picture picture;
|
|
908
|
+
auto takeColourFrames = [&] {
|
|
909
|
+
while (!shouldStop_ && colorDecoder_->receive(&picture)) {
|
|
910
|
+
if (frameIndex == 0) {
|
|
911
|
+
VLOGI("First decoded frame: %dx%d, %d-bit, chroma shift %d,%d%s", picture.width, picture.height,
|
|
912
|
+
picture.bitDepth, picture.chromaShiftX, picture.chromaShiftY, picture.rgb ? ", RGB" : "");
|
|
913
|
+
// Update stored dimensions if needed
|
|
914
|
+
width_ = picture.width;
|
|
915
|
+
height_ = picture.height;
|
|
916
|
+
}
|
|
917
|
+
auto yuvFrame = frameFromPicture(picture, frameIndex);
|
|
918
|
+
if (!yuvFrame) continue;
|
|
919
|
+
// Check before cache access - unload() may have been called
|
|
920
|
+
if (shouldStop_ || !frameCache_) break;
|
|
921
|
+
addColour(frameIndex, yuvFrame);
|
|
922
|
+
frameIndex++;
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
auto takeAlphaFrames = [&] {
|
|
926
|
+
while (!shouldStop_ && frameCache_ && alphaDecoder_->receive(&picture)) {
|
|
927
|
+
if (alphaFrameIndex == 0) {
|
|
928
|
+
VLOGI("First alpha frame: %dx%d, %d-bit, range=%d", picture.width, picture.height, picture.bitDepth,
|
|
929
|
+
picture.colorRange);
|
|
930
|
+
}
|
|
931
|
+
addAlpha(alphaFrameIndex++, alphaPlaneOf(picture, alphaAsIs_));
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
AlphaDemuxer::Packet packet;
|
|
936
|
+
while (!shouldStop_) {
|
|
937
|
+
if (!demuxer_->readPacket(&packet)) {
|
|
938
|
+
// End of file or error - drain the decoders
|
|
939
|
+
colorDecoder_->drain();
|
|
940
|
+
takeColourFrames();
|
|
941
|
+
if (alphaDecoder_) {
|
|
942
|
+
alphaDecoder_->drain();
|
|
943
|
+
takeAlphaFrames();
|
|
944
|
+
}
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (packet.stream == 0) {
|
|
949
|
+
if (colorDecoder_->send(packet.data, packet.size, packet.ptsUs)) takeColourFrames();
|
|
950
|
+
} else if (alphaDecoder_) {
|
|
951
|
+
if (packet.size == 0) {
|
|
952
|
+
// A block without alpha: its frame shows opaque
|
|
953
|
+
AlphaPlane opaque;
|
|
954
|
+
opaque.ptsUs = packet.ptsUs;
|
|
955
|
+
addAlpha(alphaFrameIndex++, std::move(opaque));
|
|
956
|
+
} else if (alphaDecoder_->send(packet.data, packet.size, packet.ptsUs)) {
|
|
957
|
+
takeAlphaFrames();
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// A colour frame whose alpha stream ended early shows opaque, as copyColour left it
|
|
963
|
+
if (!shouldStop_ && frameCache_) {
|
|
964
|
+
for (auto& [index, frame] : colourAwaitingAlpha) cacheFrame(index, frame);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Update total frames to actual count
|
|
968
|
+
totalFrames_ = frameIndex;
|
|
969
|
+
preloading_ = false;
|
|
970
|
+
|
|
971
|
+
// getFrame() returned nothing while this ran, so a paused view (at load, or after a seek)
|
|
972
|
+
// has nothing on screen yet.
|
|
973
|
+
if (!playing_ && !shouldStop_ && frameIndex > 0) {
|
|
974
|
+
int shown = std::min(currentFrame_.load(), frameIndex - 1);
|
|
975
|
+
currentFrame_ = shown;
|
|
976
|
+
emitFrame(getCachedFrame(shown));
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
auto endTime = std::chrono::high_resolution_clock::now();
|
|
980
|
+
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
|
|
981
|
+
|
|
982
|
+
// Log final cache stats (check frameCache_ not destroyed)
|
|
983
|
+
if (frameCache_ && !shouldStop_) {
|
|
984
|
+
VLOGI("📊 Preload finished: decoded %d frames, cache size=%zu (max=%d)",
|
|
985
|
+
frameIndex, frameCache_->size(), config_.cacheSize);
|
|
986
|
+
|
|
987
|
+
emitEvent("preload_complete",
|
|
988
|
+
std::to_string(frameIndex) + " frames in " +
|
|
989
|
+
std::to_string(duration.count()) + "ms");
|
|
990
|
+
} else {
|
|
991
|
+
VLOGI("📊 Preload interrupted: decoded %d frames before stop", frameIndex);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
void WebMAlphaPlayer::play() {
|
|
996
|
+
if (!loaded_) return;
|
|
997
|
+
|
|
998
|
+
// If already playing, nothing to do
|
|
999
|
+
if (playing_) return;
|
|
1000
|
+
|
|
1001
|
+
playing_ = true;
|
|
1002
|
+
shouldStop_ = false;
|
|
1003
|
+
pendingSeekFrame_ = -1; // The playback loop shows the sought frame from here on
|
|
1004
|
+
|
|
1005
|
+
// Only create a new thread if one isn't already running
|
|
1006
|
+
// (pause just sets playing_ = false, thread keeps running). After a clip that doesn't loop
|
|
1007
|
+
// has ended, its thread has returned but is still joinable.
|
|
1008
|
+
if (playbackThread_ && playbackThread_->joinable() && !playbackLoopRunning_) {
|
|
1009
|
+
playbackThread_->join();
|
|
1010
|
+
playbackThread_.reset();
|
|
1011
|
+
}
|
|
1012
|
+
if (!playbackThread_ || !playbackThread_->joinable()) {
|
|
1013
|
+
playbackLoopRunning_ = true;
|
|
1014
|
+
playbackThread_ = std::make_unique<std::thread>(&WebMAlphaPlayer::playbackLoop, this);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
emitEvent("play");
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
void WebMAlphaPlayer::pause() {
|
|
1021
|
+
playing_ = false;
|
|
1022
|
+
emitEvent("pause");
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
void WebMAlphaPlayer::stop() {
|
|
1026
|
+
shouldStop_ = true;
|
|
1027
|
+
playing_ = false;
|
|
1028
|
+
currentFrame_ = 0;
|
|
1029
|
+
|
|
1030
|
+
decodeCV_.notify_all();
|
|
1031
|
+
|
|
1032
|
+
if (playbackThread_ && playbackThread_->joinable()) {
|
|
1033
|
+
playbackThread_->join();
|
|
1034
|
+
}
|
|
1035
|
+
playbackThread_.reset();
|
|
1036
|
+
|
|
1037
|
+
emitEvent("stop");
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
void WebMAlphaPlayer::seekTo(double timeSeconds) {
|
|
1041
|
+
if (!(timeSeconds > 0)) timeSeconds = 0; // Also catches NaN
|
|
1042
|
+
// The epsilon keeps a time computed as frame / fps from rounding down to the frame before.
|
|
1043
|
+
seekToFrame(static_cast<int>(std::floor(timeSeconds * frameRate_ + 1e-6)));
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
void WebMAlphaPlayer::setPlaybackSpeed(double speed) {
|
|
1047
|
+
// A rate at or below zero would make the frame interval infinite or negative. Pausing is
|
|
1048
|
+
// `pause()`; this clamps to something slow rather than wedging the playback thread.
|
|
1049
|
+
const double clamped = speed > 0.01 ? speed : 0.01;
|
|
1050
|
+
playbackSpeed_ = clamped;
|
|
1051
|
+
config_.playbackSpeed = clamped;
|
|
1052
|
+
VLOGI("🎬 Playback speed set to %.3f", clamped);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
double WebMAlphaPlayer::getPlaybackSpeed() const {
|
|
1056
|
+
return playbackSpeed_.load();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
void WebMAlphaPlayer::seekToFrame(int frameIndex) {
|
|
1060
|
+
if (!loaded_ || totalFrames_ <= 0) return;
|
|
1061
|
+
frameIndex = std::max(0, std::min(frameIndex, totalFrames_ - 1));
|
|
1062
|
+
currentFrame_ = frameIndex;
|
|
1063
|
+
|
|
1064
|
+
if (config_.mode == PlaybackMode::Preload) {
|
|
1065
|
+
auto frame = getFrame(frameIndex);
|
|
1066
|
+
if (frame) emitFrame(frame);
|
|
1067
|
+
} else if (auto frame = getCachedFrame(frameIndex)) {
|
|
1068
|
+
pendingSeekFrame_ = -1;
|
|
1069
|
+
emitFrame(frame);
|
|
1070
|
+
} else {
|
|
1071
|
+
// The prefetch thread owns the decoder, so it decodes the frame and, if still paused by
|
|
1072
|
+
// then, shows it. StreamAndPreload decodes the whole clip in order, so it only waits.
|
|
1073
|
+
pendingSeekFrame_ = frameIndex;
|
|
1074
|
+
int decodePos = streamDecodePos_.load();
|
|
1075
|
+
int prefetchAhead = config_.streamPrefetchAhead > 0 ? config_.streamPrefetchAhead : 8;
|
|
1076
|
+
bool decodingTowardsIt = frameIndex >= decodePos && frameIndex - decodePos < prefetchAhead;
|
|
1077
|
+
if (config_.mode == PlaybackMode::Stream && !decodingTowardsIt) {
|
|
1078
|
+
seekRequest_ = frameIndex;
|
|
1079
|
+
}
|
|
1080
|
+
requestPrefetch(frameIndex);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
emitEvent("seek", std::to_string(frameIndex));
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
int WebMAlphaPlayer::frameIndexForPts(int64_t ptsUs) const {
|
|
1087
|
+
return static_cast<int>(std::llround((ptsUs - startUs_) / 1e6 * frameRate_));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
void WebMAlphaPlayer::playbackLoop() {
|
|
1091
|
+
auto lastFrameTime = std::chrono::high_resolution_clock::now();
|
|
1092
|
+
|
|
1093
|
+
VLOGI("🎬 Playback loop started: frameRate=%.2f, speed=%.2f",
|
|
1094
|
+
frameRate_, playbackSpeed_.load());
|
|
1095
|
+
|
|
1096
|
+
int framesEmitted = 0;
|
|
1097
|
+
auto loopStartTime = std::chrono::high_resolution_clock::now();
|
|
1098
|
+
|
|
1099
|
+
// Per pass in Stream mode, for tuning streamPrefetchAhead: the fewest frames decoded after
|
|
1100
|
+
// the shown one, and how often playback waited for the decoder
|
|
1101
|
+
int fewestAhead = totalFrames_;
|
|
1102
|
+
int stalls = 0;
|
|
1103
|
+
bool stalled = false;
|
|
1104
|
+
auto logPassBuffer = [&]() {
|
|
1105
|
+
if (config_.mode != PlaybackMode::Stream) return;
|
|
1106
|
+
VLOGI("📺 Pass buffer: fewest frames ahead %d of %d, stalls %d",
|
|
1107
|
+
fewestAhead, config_.streamPrefetchAhead, stalls);
|
|
1108
|
+
fewestAhead = totalFrames_;
|
|
1109
|
+
stalls = 0;
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
while (!shouldStop_ && loaded_) {
|
|
1113
|
+
if (!playing_) {
|
|
1114
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(16));
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Read per tick, not once before the loop.
|
|
1120
|
+
*
|
|
1121
|
+
* The rate can change while playing — that is the whole point of `setPlaybackSpeed` —
|
|
1122
|
+
* and a duration hoisted out of the loop would pin the speed to whatever it was when
|
|
1123
|
+
* playback happened to start. Only the interval changes; the frame index still advances
|
|
1124
|
+
* by one per tick below, so speeding up never moves the playhead.
|
|
1125
|
+
*/
|
|
1126
|
+
const double frameDurationMs = 1000.0 / (frameRate_ * playbackSpeed_.load());
|
|
1127
|
+
|
|
1128
|
+
auto now = std::chrono::high_resolution_clock::now();
|
|
1129
|
+
auto elapsed = std::chrono::duration<double, std::milli>(now - lastFrameTime);
|
|
1130
|
+
|
|
1131
|
+
if (elapsed.count() >= frameDurationMs) {
|
|
1132
|
+
const int shownFrame = currentFrame_.load();
|
|
1133
|
+
int frameIdx = shownFrame;
|
|
1134
|
+
auto frame = getFrame(frameIdx);
|
|
1135
|
+
|
|
1136
|
+
if (frame) {
|
|
1137
|
+
emitFrame(frame);
|
|
1138
|
+
framesEmitted++;
|
|
1139
|
+
stalled = false;
|
|
1140
|
+
int ahead = streamDecodePos_.load() - frameIdx - 1;
|
|
1141
|
+
if (ahead < 0) ahead += totalFrames_; // the decoder has wrapped to the start
|
|
1142
|
+
fewestAhead = std::min(fewestAhead, ahead);
|
|
1143
|
+
|
|
1144
|
+
// Log FPS every 30 frames
|
|
1145
|
+
if (framesEmitted % 30 == 0) {
|
|
1146
|
+
auto totalElapsed = std::chrono::duration<double>(now - loopStartTime).count();
|
|
1147
|
+
double actualFps = framesEmitted / totalElapsed;
|
|
1148
|
+
VLOGI("🎬 Playback stats: frame=%d, emitted=%d, actualFPS=%.1f, targetFPS=%.1f",
|
|
1149
|
+
frameIdx, framesEmitted, actualFps, frameRate_);
|
|
1150
|
+
}
|
|
1151
|
+
} else if (preloading_ || config_.mode == PlaybackMode::Stream || config_.mode == PlaybackMode::StreamAndPreload) {
|
|
1152
|
+
// Frame not decoded yet - don't advance, wait for the preload or prefetch
|
|
1153
|
+
if (!stalled) {
|
|
1154
|
+
stalled = true;
|
|
1155
|
+
stalls++;
|
|
1156
|
+
}
|
|
1157
|
+
VLOGI("🎬 Frame %d not ready, waiting for prefetch (decode at %d)", frameIdx, streamDecodePos_.load());
|
|
1158
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
1159
|
+
continue; // Try again without advancing frame
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// In streaming modes, request async prefetch and evict old frames
|
|
1163
|
+
// (evictOldFrames is a no-op in StreamAndPreload mode)
|
|
1164
|
+
if (config_.mode == PlaybackMode::Stream || config_.mode == PlaybackMode::StreamAndPreload) {
|
|
1165
|
+
requestPrefetch(frameIdx); // Non-blocking - signals background thread
|
|
1166
|
+
evictOldFrames(frameIdx);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Advance frame
|
|
1170
|
+
frameIdx++;
|
|
1171
|
+
if (frameIdx >= totalFrames_) {
|
|
1172
|
+
logPassBuffer();
|
|
1173
|
+
if (config_.loopPlayback) {
|
|
1174
|
+
frameIdx = 0;
|
|
1175
|
+
// In streaming modes, request prefetch from start on loop
|
|
1176
|
+
// (In StreamAndPreload, frames are already cached so this is just a hint)
|
|
1177
|
+
if (config_.mode == PlaybackMode::Stream || config_.mode == PlaybackMode::StreamAndPreload) {
|
|
1178
|
+
requestPrefetch(0); // Non-blocking
|
|
1179
|
+
}
|
|
1180
|
+
// Reset FPS tracking on loop
|
|
1181
|
+
framesEmitted = 0;
|
|
1182
|
+
loopStartTime = now;
|
|
1183
|
+
emitEvent("loop");
|
|
1184
|
+
} else {
|
|
1185
|
+
playing_ = false;
|
|
1186
|
+
emitEvent("ended");
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
// A seek during this tick has already moved the playhead; keep it.
|
|
1191
|
+
int expected = shownFrame;
|
|
1192
|
+
currentFrame_.compare_exchange_strong(expected, frameIdx);
|
|
1193
|
+
lastFrameTime = now;
|
|
1194
|
+
} else {
|
|
1195
|
+
// Sleep until next frame is due (avoid busy waiting)
|
|
1196
|
+
auto sleepTime = frameDurationMs - elapsed.count();
|
|
1197
|
+
if (sleepTime > 1.0) {
|
|
1198
|
+
std::this_thread::sleep_for(std::chrono::microseconds(
|
|
1199
|
+
static_cast<int64_t>((sleepTime - 1.0) * 1000)
|
|
1200
|
+
));
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
VLOGI("🎬 Playback loop ended: shouldStop=%d, loaded=%d", shouldStop_.load(), loaded_.load());
|
|
1206
|
+
playbackLoopRunning_ = false;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
void WebMAlphaPlayer::streamingPrefetch(int currentFrame) {
|
|
1210
|
+
if (!frameCache_ || !loaded_) return;
|
|
1211
|
+
|
|
1212
|
+
int prefetchAhead = config_.streamPrefetchAhead > 0 ? config_.streamPrefetchAhead : 8;
|
|
1213
|
+
int endFrame = std::min(currentFrame + prefetchAhead, totalFrames_);
|
|
1214
|
+
|
|
1215
|
+
// Handle loop wraparound
|
|
1216
|
+
if (config_.loopPlayback && endFrame < currentFrame + prefetchAhead) {
|
|
1217
|
+
// Prefetch remaining frames from start
|
|
1218
|
+
int wrapFrames = (currentFrame + prefetchAhead) - totalFrames_;
|
|
1219
|
+
for (int i = 0; i < wrapFrames && !shouldStop_; i++) {
|
|
1220
|
+
std::string key = makeFrameKey(i);
|
|
1221
|
+
if (!frameCache_->contains(key)) {
|
|
1222
|
+
decodeFrame(i);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Prefetch frames ahead of current position
|
|
1228
|
+
for (int i = currentFrame + 1; i < endFrame && !shouldStop_; i++) {
|
|
1229
|
+
std::string key = makeFrameKey(i);
|
|
1230
|
+
if (!frameCache_->contains(key)) {
|
|
1231
|
+
decodeFrame(i);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
void WebMAlphaPlayer::evictOldFrames(int currentFrame) {
|
|
1237
|
+
if (!frameCache_ || !loaded_ || totalFrames_ <= 0) return;
|
|
1238
|
+
|
|
1239
|
+
// In StreamAndPreload (hybrid) mode, never evict frames - we want to keep all of them
|
|
1240
|
+
if (config_.mode == PlaybackMode::StreamAndPreload) {
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Check each cached frame and evict if behind playback
|
|
1245
|
+
// This keeps the shown frame and the ones decoded after it
|
|
1246
|
+
for (int i = 0; i < totalFrames_ && !shouldStop_; i++) {
|
|
1247
|
+
std::string key = makeFrameKey(i);
|
|
1248
|
+
if (!frameCache_->contains(key)) continue;
|
|
1249
|
+
|
|
1250
|
+
// Calculate circular distance from currentFrame to frame i
|
|
1251
|
+
// Positive = ahead of playback, negative = behind playback
|
|
1252
|
+
int dist = i - currentFrame;
|
|
1253
|
+
if (config_.loopPlayback) {
|
|
1254
|
+
// Normalize to [-totalFrames/2, totalFrames/2] for proper wraparound
|
|
1255
|
+
if (dist > totalFrames_ / 2) dist -= totalFrames_;
|
|
1256
|
+
if (dist < -totalFrames_ / 2) dist += totalFrames_;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Evict frames that are behind playback (already played)
|
|
1260
|
+
if (dist < 0) {
|
|
1261
|
+
frameCache_->evict(key);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
void WebMAlphaPlayer::requestPrefetch(int currentFrame) {
|
|
1267
|
+
// Non-blocking: just update the target and signal the prefetch thread
|
|
1268
|
+
prefetchTarget_ = currentFrame;
|
|
1269
|
+
prefetchCV_.notify_one();
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
void WebMAlphaPlayer::prefetchLoop() {
|
|
1273
|
+
VLOGI("📺 Prefetch thread started (sequential decode): shouldStop=%d, loaded=%d, mode=%d",
|
|
1274
|
+
shouldStop_.load(), loaded_.load(), static_cast<int>(config_.mode));
|
|
1275
|
+
|
|
1276
|
+
// Acquire decode mutex - we'll hold it for sequential decoding
|
|
1277
|
+
std::lock_guard<std::timed_mutex> decodeLock(decodeMutex_);
|
|
1278
|
+
|
|
1279
|
+
AlphaDemuxer::Packet packet;
|
|
1280
|
+
Picture picture;
|
|
1281
|
+
|
|
1282
|
+
int frameIndex = streamDecodePos_.load();
|
|
1283
|
+
int prefetchAhead = config_.streamPrefetchAhead > 0 ? config_.streamPrefetchAhead : 8;
|
|
1284
|
+
bool isHybridMode = (config_.mode == PlaybackMode::StreamAndPreload);
|
|
1285
|
+
auto passStart = std::chrono::high_resolution_clock::now();
|
|
1286
|
+
|
|
1287
|
+
VLOGI("📺 Starting sequential decode from frame %d, prefetchAhead=%d, hybrid=%d",
|
|
1288
|
+
frameIndex, prefetchAhead, isHybridMode);
|
|
1289
|
+
|
|
1290
|
+
// After a seek: the frames before seekTarget are decoded only to reach it, and the frame
|
|
1291
|
+
// index is re-read from the first frame's timestamp, since the demuxer lands on a keyframe.
|
|
1292
|
+
int seekTarget = 0;
|
|
1293
|
+
bool resyncFromPts = false;
|
|
1294
|
+
|
|
1295
|
+
// Two-stream files: a colour frame isn't cached until the alpha frame with its timestamp is
|
|
1296
|
+
// decoded, so a streamed frame never shows without its alpha. Colour packets read while
|
|
1297
|
+
// looking for that alpha frame are held and decoded afterwards; alpha frames decoded ahead of
|
|
1298
|
+
// their colour frame wait in alphaAhead.
|
|
1299
|
+
struct HeldPacket {
|
|
1300
|
+
std::vector<uint8_t> data;
|
|
1301
|
+
int64_t ptsUs;
|
|
1302
|
+
};
|
|
1303
|
+
std::deque<HeldPacket> heldColourPackets;
|
|
1304
|
+
std::deque<AlphaPlane> alphaAhead;
|
|
1305
|
+
auto dropHeld = [&] {
|
|
1306
|
+
heldColourPackets.clear();
|
|
1307
|
+
alphaAhead.clear();
|
|
1308
|
+
};
|
|
1309
|
+
// <0: the alpha frame is for an earlier colour frame, 0: this one, >0: a later one.
|
|
1310
|
+
auto compareToColour = [&](const AlphaPlane& alpha, int64_t colourPtsUs) {
|
|
1311
|
+
double gapFrames = (alpha.ptsUs - colourPtsUs) / 1e6 * frameRate_;
|
|
1312
|
+
return gapFrames < -0.5 ? -1 : gapFrames > 0.5 ? 1 : 0;
|
|
1313
|
+
};
|
|
1314
|
+
auto keepAlphaFrames = [&] {
|
|
1315
|
+
Picture alphaPicture;
|
|
1316
|
+
while (alphaDecoder_->receive(&alphaPicture)) alphaAhead.push_back(alphaPlaneOf(alphaPicture, alphaAsIs_));
|
|
1317
|
+
while (alphaAhead.size() > 8) alphaAhead.pop_front();
|
|
1318
|
+
};
|
|
1319
|
+
// An alpha packet: decoded, or an empty one (a block without alpha) kept as an opaque frame
|
|
1320
|
+
auto takeAlphaPacket = [&](const AlphaDemuxer::Packet& p) {
|
|
1321
|
+
if (p.size == 0) {
|
|
1322
|
+
AlphaPlane opaque;
|
|
1323
|
+
opaque.ptsUs = p.ptsUs;
|
|
1324
|
+
alphaAhead.push_back(std::move(opaque));
|
|
1325
|
+
} else if (alphaDecoder_->send(p.data, p.size, p.ptsUs)) {
|
|
1326
|
+
keepAlphaFrames();
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
// Decoders with frame delay (dav1d's frame threads) hold the last frames until they're
|
|
1330
|
+
// drained at the end of the file.
|
|
1331
|
+
bool colourDrained = false;
|
|
1332
|
+
bool alphaDrained = false;
|
|
1333
|
+
// After the demuxer has moved
|
|
1334
|
+
auto restartDecoding = [&] {
|
|
1335
|
+
colorDecoder_->flush();
|
|
1336
|
+
if (alphaDecoder_) {
|
|
1337
|
+
alphaDecoder_->flush();
|
|
1338
|
+
}
|
|
1339
|
+
dropHeld();
|
|
1340
|
+
colourDrained = false;
|
|
1341
|
+
alphaDrained = false;
|
|
1342
|
+
};
|
|
1343
|
+
auto attachAlpha = [&](YUVAFrame& frame, int64_t colourPtsUs) {
|
|
1344
|
+
AlphaDemuxer::Packet p;
|
|
1345
|
+
bool attached = false;
|
|
1346
|
+
while (!attached && !shouldStop_) {
|
|
1347
|
+
while (!alphaAhead.empty()) {
|
|
1348
|
+
int order = compareToColour(alphaAhead.front(), colourPtsUs);
|
|
1349
|
+
if (order > 0) break;
|
|
1350
|
+
if (order == 0) {
|
|
1351
|
+
copyAlpha(frame, alphaAhead.front());
|
|
1352
|
+
attached = true;
|
|
1353
|
+
}
|
|
1354
|
+
alphaAhead.pop_front();
|
|
1355
|
+
if (attached) break;
|
|
1356
|
+
}
|
|
1357
|
+
if (attached || !alphaAhead.empty()) break;
|
|
1358
|
+
if (!demuxer_->readPacket(&p)) {
|
|
1359
|
+
if (alphaDrained) break;
|
|
1360
|
+
alphaDrained = true;
|
|
1361
|
+
alphaDecoder_->drain();
|
|
1362
|
+
keepAlphaFrames();
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
if (p.stream == 0) {
|
|
1366
|
+
heldColourPackets.push_back({std::vector<uint8_t>(p.data, p.data + p.size), p.ptsUs});
|
|
1367
|
+
} else {
|
|
1368
|
+
takeAlphaPacket(p);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1373
|
+
// Turns every colour frame the decoder has ready into a cached frame
|
|
1374
|
+
auto takeColourFrames = [&](int playbackPos) {
|
|
1375
|
+
while (!shouldStop_ && colorDecoder_->receive(&picture)) {
|
|
1376
|
+
if (resyncFromPts) {
|
|
1377
|
+
resyncFromPts = false;
|
|
1378
|
+
frameIndex = frameIndexForPts(picture.ptsUs);
|
|
1379
|
+
}
|
|
1380
|
+
if (frameIndex < seekTarget) {
|
|
1381
|
+
frameIndex++;
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
seekTarget = 0;
|
|
1385
|
+
|
|
1386
|
+
// Check if we already have this frame cached
|
|
1387
|
+
std::string key = makeFrameKey(frameIndex);
|
|
1388
|
+
if (frameCache_ && !frameCache_->contains(key)) {
|
|
1389
|
+
const int64_t colourPtsUs = picture.ptsUs;
|
|
1390
|
+
if (auto yuvFrame = frameFromPicture(picture, frameIndex)) {
|
|
1391
|
+
if (alphaDecoder_) attachAlpha(*yuvFrame, colourPtsUs);
|
|
1392
|
+
|
|
1393
|
+
// Put frame in cache - eviction is handled by playback thread
|
|
1394
|
+
// via evictOldFrames() after consuming each frame
|
|
1395
|
+
frameCache_->put(key, yuvFrame);
|
|
1396
|
+
stats_.framesDecoded++;
|
|
1397
|
+
stats_.framesCached++;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
int sought = frameIndex;
|
|
1402
|
+
if (!playing_ && pendingSeekFrame_.compare_exchange_strong(sought, -1)) {
|
|
1403
|
+
emitFrame(getCachedFrame(frameIndex));
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
frameIndex++;
|
|
1407
|
+
streamDecodePos_ = frameIndex;
|
|
1408
|
+
|
|
1409
|
+
// Log progress periodically
|
|
1410
|
+
if (frameIndex % 30 == 0) {
|
|
1411
|
+
VLOGI("📺 Decoded frame %d, playback at %d, cache size=%zu",
|
|
1412
|
+
frameIndex, playbackPos, frameCache_ ? frameCache_->size() : 0);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
while (!shouldStop_ && loaded_) {
|
|
1418
|
+
int requested = seekRequest_.exchange(-1);
|
|
1419
|
+
if (requested >= 0) {
|
|
1420
|
+
demuxer_->seekToTimeUs(startUs_ + static_cast<int64_t>(std::llround(requested / frameRate_ * 1e6)));
|
|
1421
|
+
restartDecoding();
|
|
1422
|
+
frameIndex = requested;
|
|
1423
|
+
streamDecodePos_ = requested;
|
|
1424
|
+
seekTarget = requested;
|
|
1425
|
+
resyncFromPts = true;
|
|
1426
|
+
VLOGI("📺 Seek: decoding from the keyframe before frame %d", requested);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
int playbackPos = prefetchTarget_.load();
|
|
1430
|
+
if (playbackPos < 0) playbackPos = 0;
|
|
1431
|
+
|
|
1432
|
+
// In hybrid mode, once we've decoded all frames, we're done - no need to re-decode on loop
|
|
1433
|
+
if (isHybridMode && firstPassComplete_) {
|
|
1434
|
+
// All frames cached, just sleep and wait for stop signal
|
|
1435
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// In stream mode (not hybrid), check if we're far enough ahead and should PAUSE
|
|
1440
|
+
// Use circular arithmetic to handle wraparound at loop boundaries
|
|
1441
|
+
if (!isHybridMode) {
|
|
1442
|
+
// Calculate how far ahead decode is from playback (circular)
|
|
1443
|
+
int aheadBy = frameIndex - playbackPos;
|
|
1444
|
+
if (config_.loopPlayback && totalFrames_ > 0) {
|
|
1445
|
+
// Normalize to [0, totalFrames) range for circular comparison
|
|
1446
|
+
if (aheadBy < 0) aheadBy += totalFrames_;
|
|
1447
|
+
if (aheadBy > totalFrames_ / 2) {
|
|
1448
|
+
// We're actually behind (wrapped around), not ahead
|
|
1449
|
+
aheadBy = aheadBy - totalFrames_;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// If we're sufficiently ahead, PAUSE and wait for playback to catch up. aheadBy
|
|
1454
|
+
// counts the shown frame, so this keeps prefetchAhead frames after it.
|
|
1455
|
+
if (aheadBy > prefetchAhead) {
|
|
1456
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// In hybrid mode, decode to the end
|
|
1462
|
+
if (isHybridMode && frameIndex >= totalFrames_) {
|
|
1463
|
+
if (!firstPassComplete_) {
|
|
1464
|
+
firstPassComplete_ = true;
|
|
1465
|
+
VLOGI("📺 StreamAndPreload: First pass complete! All %d frames cached", totalFrames_);
|
|
1466
|
+
auto passMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
1467
|
+
std::chrono::high_resolution_clock::now() - passStart).count();
|
|
1468
|
+
emitEvent("preload_complete", std::to_string(totalFrames_) + " frames in " +
|
|
1469
|
+
std::to_string(passMs) + "ms");
|
|
1470
|
+
}
|
|
1471
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// Handle reaching end of file (stream mode only - hybrid handled above)
|
|
1476
|
+
if (frameIndex >= totalFrames_) {
|
|
1477
|
+
if (config_.loopPlayback) {
|
|
1478
|
+
// Stream mode: Seek back to start for looping
|
|
1479
|
+
demuxer_->seekToStart();
|
|
1480
|
+
restartDecoding();
|
|
1481
|
+
frameIndex = 0;
|
|
1482
|
+
streamDecodePos_ = 0;
|
|
1483
|
+
seekTarget = 0;
|
|
1484
|
+
VLOGI("📺 Prefetch reached EOF, looping back to frame 0 (cache preserved for playback)");
|
|
1485
|
+
} else {
|
|
1486
|
+
// Not looping, wait for playback to end
|
|
1487
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(16));
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Read and decode next frame sequentially (no seeking!)
|
|
1493
|
+
if (!heldColourPackets.empty()) {
|
|
1494
|
+
HeldPacket held = std::move(heldColourPackets.front());
|
|
1495
|
+
heldColourPackets.pop_front();
|
|
1496
|
+
if (colorDecoder_->send(held.data.data(), static_cast<int>(held.data.size()), held.ptsUs)) {
|
|
1497
|
+
takeColourFrames(playbackPos);
|
|
1498
|
+
}
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
if (!demuxer_->readPacket(&packet)) {
|
|
1502
|
+
if (shouldStop_ || ioAbort_) break;
|
|
1503
|
+
// The input itself failed, e.g. the network went away: looping would replay only the
|
|
1504
|
+
// part that arrived, so stop decoding instead.
|
|
1505
|
+
if (demuxer_->failed()) {
|
|
1506
|
+
LOGE("📺 Read failed at frame %d, stopping decode", frameIndex);
|
|
1507
|
+
std::string why = reader_ ? reader_->lastError() : "";
|
|
1508
|
+
emitEvent("error", "Reading the video failed" + (why.empty() ? std::string() : ": " + why));
|
|
1509
|
+
break;
|
|
1510
|
+
}
|
|
1511
|
+
// End of file: take the frames the decoder still holds, then let the frame count
|
|
1512
|
+
// checks above loop or finish. A file with fewer frames than estimated loops here.
|
|
1513
|
+
if (!colourDrained) {
|
|
1514
|
+
colourDrained = true;
|
|
1515
|
+
colorDecoder_->drain();
|
|
1516
|
+
takeColourFrames(playbackPos);
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
if (config_.loopPlayback) {
|
|
1520
|
+
demuxer_->seekToStart();
|
|
1521
|
+
restartDecoding();
|
|
1522
|
+
frameIndex = 0;
|
|
1523
|
+
streamDecodePos_ = 0;
|
|
1524
|
+
seekTarget = 0;
|
|
1525
|
+
VLOGI("📺 End of file, looping back to frame 0 (cache preserved)");
|
|
1526
|
+
} else {
|
|
1527
|
+
// Not looping: wait for playback to end
|
|
1528
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(16));
|
|
1529
|
+
}
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
if (packet.stream == 0) {
|
|
1534
|
+
if (colorDecoder_->send(packet.data, packet.size, packet.ptsUs)) takeColourFrames(playbackPos);
|
|
1535
|
+
} else if (alphaDecoder_) {
|
|
1536
|
+
takeAlphaPacket(packet);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
VLOGI("📺 Prefetch thread stopped at frame %d", frameIndex);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
std::shared_ptr<YUVAFrame> WebMAlphaPlayer::getFrame(int index) {
|
|
1544
|
+
if (!loaded_ || index < 0 || index >= totalFrames_ || !frameCache_) {
|
|
1545
|
+
return nullptr;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Try cache first
|
|
1549
|
+
std::string key = makeFrameKey(index);
|
|
1550
|
+
auto frame = frameCache_->get(key);
|
|
1551
|
+
|
|
1552
|
+
if (frame) {
|
|
1553
|
+
return frame;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// While the preload pass runs it holds the decoder and will cache this frame shortly.
|
|
1557
|
+
// Decoding on demand would wait for the whole pass, on the UI thread when called at load.
|
|
1558
|
+
if (preloading_) {
|
|
1559
|
+
return nullptr;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// In streaming modes (Stream or StreamAndPreload), don't block - return null and let prefetch thread catch up
|
|
1563
|
+
// The playback loop will show the last valid frame or skip
|
|
1564
|
+
if (config_.mode == PlaybackMode::Stream || config_.mode == PlaybackMode::StreamAndPreload) {
|
|
1565
|
+
// Request prefetch to catch up
|
|
1566
|
+
requestPrefetch(index);
|
|
1567
|
+
return nullptr;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// In preload mode, decode on demand (shouldn't happen if preloadAllFrames is true)
|
|
1571
|
+
return decodeFrame(index);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
std::shared_ptr<YUVAFrame> WebMAlphaPlayer::getCachedFrame(int index) {
|
|
1575
|
+
if (!loaded_ || index < 0 || index >= totalFrames_ || !frameCache_) {
|
|
1576
|
+
return nullptr;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Cache-only lookup - never triggers decoding
|
|
1580
|
+
// Safe to call from any thread
|
|
1581
|
+
std::string key = makeFrameKey(index);
|
|
1582
|
+
return frameCache_->get(key);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
std::shared_ptr<YUVAFrame> WebMAlphaPlayer::getCurrentFrameData() {
|
|
1586
|
+
return getFrame(currentFrame_);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
bool WebMAlphaPlayer::isLoaded() const { return loaded_; }
|
|
1590
|
+
bool WebMAlphaPlayer::isPlaying() const { return playing_; }
|
|
1591
|
+
int WebMAlphaPlayer::getCurrentFrame() const { return currentFrame_; }
|
|
1592
|
+
double WebMAlphaPlayer::getCurrentTime() const {
|
|
1593
|
+
return currentFrame_ / frameRate_;
|
|
1594
|
+
}
|
|
1595
|
+
double WebMAlphaPlayer::getDuration() const { return duration_; }
|
|
1596
|
+
int WebMAlphaPlayer::getTotalFrames() const { return totalFrames_; }
|
|
1597
|
+
int WebMAlphaPlayer::getWidth() const { return width_; }
|
|
1598
|
+
int WebMAlphaPlayer::getHeight() const { return height_; }
|
|
1599
|
+
double WebMAlphaPlayer::getFrameRate() const { return frameRate_; }
|
|
1600
|
+
|
|
1601
|
+
YuvToRgb WebMAlphaPlayer::getYuvToRgb() const {
|
|
1602
|
+
std::lock_guard<std::mutex> lock(yuvToRgbMutex_);
|
|
1603
|
+
return yuvToRgb_;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
void WebMAlphaPlayer::setFrameCallback(FrameCallback callback) {
|
|
1607
|
+
std::lock_guard<std::mutex> lock(callbackMutex_);
|
|
1608
|
+
frameCallback_ = std::move(callback);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
void WebMAlphaPlayer::setEventCallback(EventCallback callback) {
|
|
1612
|
+
std::lock_guard<std::mutex> lock(callbackMutex_);
|
|
1613
|
+
eventCallback_ = std::move(callback);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
DecoderStatsSnapshot WebMAlphaPlayer::getStats() const {
|
|
1617
|
+
return stats_.snapshot();
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
void WebMAlphaPlayer::resetStats() {
|
|
1621
|
+
stats_.reset();
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
void WebMAlphaPlayer::emitEvent(const std::string& event, const std::string& data) {
|
|
1625
|
+
if (event == "error") {
|
|
1626
|
+
std::lock_guard<std::mutex> lock(lastErrorCodeMutex_);
|
|
1627
|
+
lastErrorCode_.clear();
|
|
1628
|
+
}
|
|
1629
|
+
std::lock_guard<std::mutex> lock(callbackMutex_);
|
|
1630
|
+
if (eventCallback_) {
|
|
1631
|
+
eventCallback_(event, data);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
void WebMAlphaPlayer::emitError(const std::string& code, const std::string& message) {
|
|
1636
|
+
std::lock_guard<std::mutex> lock(callbackMutex_);
|
|
1637
|
+
{
|
|
1638
|
+
std::lock_guard<std::mutex> codeLock(lastErrorCodeMutex_);
|
|
1639
|
+
lastErrorCode_ = code;
|
|
1640
|
+
}
|
|
1641
|
+
if (eventCallback_) {
|
|
1642
|
+
eventCallback_("error", message);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
std::string WebMAlphaPlayer::getLastErrorCode() const {
|
|
1647
|
+
std::lock_guard<std::mutex> lock(lastErrorCodeMutex_);
|
|
1648
|
+
return lastErrorCode_;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
void WebMAlphaPlayer::emitFrame(std::shared_ptr<YUVAFrame> frame) {
|
|
1652
|
+
std::lock_guard<std::mutex> lock(callbackMutex_);
|
|
1653
|
+
if (frameCallback_ && frame) {
|
|
1654
|
+
frameCallback_(frame);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
std::string WebMAlphaPlayer::makeFrameKey(int index) const {
|
|
1659
|
+
return std::to_string(index);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
std::string WebMAlphaPlayer::getLibraryVersions() {
|
|
1663
|
+
return decoderLibraryVersions();
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
} // namespace webm
|