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.
Files changed (283) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +903 -2
  3. package/THIRD_PARTY_NOTICES.md +30 -0
  4. package/android/av1/build.gradle +67 -0
  5. package/android/av1/consumer-rules.pro +4 -0
  6. package/android/av1/src/main/cpp/Av1Codec_jni.cpp +8 -0
  7. package/android/av1/src/main/cpp/CMakeLists.txt +34 -0
  8. package/android/av1/src/main/java/dev/mediacache/alphavideo/av1/Av1Codec.java +21 -0
  9. package/android/build.gradle +85 -0
  10. package/android/consumer-rules.pro +2 -0
  11. package/android/gradle.properties +1 -0
  12. package/android/publish.gradle +82 -0
  13. package/android/settings.gradle +5 -0
  14. package/android/src/main/AndroidManifest.xml +1 -0
  15. package/android/src/main/cpp/CMakeLists.txt +59 -0
  16. package/android/src/main/java/dev/mediacache/alphavideo/AlphaVideoCodecs.java +73 -0
  17. package/android/src/main/java/dev/mediacache/alphavideo/AlphaVideoView.java +1966 -0
  18. package/android/src/main/java/dev/mediacache/alphavideo/BufferedDualStreamPlayer.java +978 -0
  19. package/android/src/main/java/dev/mediacache/alphavideo/Fetcher.java +37 -0
  20. package/android/src/main/java/dev/mediacache/alphavideo/HttpFetcher.java +156 -0
  21. package/android/src/main/java/dev/mediacache/alphavideo/MediaCodecColor.java +47 -0
  22. package/android/src/main/java/dev/mediacache/alphavideo/MediaCodecDecoders.java +135 -0
  23. package/android/src/main/java/dev/mediacache/alphavideo/RangeSourceLink.java +54 -0
  24. package/android/src/main/java/dev/mediacache/alphavideo/SourceReader.java +174 -0
  25. package/android/src/main/java/dev/mediacache/alphavideo/SurfaceTextureVideoDecoder.java +802 -0
  26. package/android/src/main/java/dev/mediacache/alphavideo/WebMAlphaPlayerNative.java +95 -0
  27. package/android/src/main/java/dev/mediacache/alphavideo/WebMFrame.java +54 -0
  28. package/android/src/main/java/dev/mediacache/alphavideo/WebMSource.java +160 -0
  29. package/android/src/rangesource/java/dev/mediacache/alphavideo/AppDataSource.java +50 -0
  30. package/android/vpx/build.gradle +67 -0
  31. package/android/vpx/consumer-rules.pro +4 -0
  32. package/android/vpx/src/main/cpp/CMakeLists.txt +34 -0
  33. package/android/vpx/src/main/cpp/VpxCodec_jni.cpp +8 -0
  34. package/android/vpx/src/main/java/dev/mediacache/alphavideo/vpx/VpxCodec.java +21 -0
  35. package/codecs/av1/Dav1dDecoderPlugin.cpp +157 -0
  36. package/codecs/av1/Dav1dDecoderPlugin.h +17 -0
  37. package/codecs/vpx/VpxDecoderPlugin.cpp +103 -0
  38. package/codecs/vpx/VpxDecoderPlugin.h +17 -0
  39. package/cpp/AlphaDemuxer.cpp +984 -0
  40. package/cpp/AlphaDemuxer.h +123 -0
  41. package/cpp/AlphaVideoDecoderPlugin.h +84 -0
  42. package/cpp/Av1SequenceHeader.cpp +175 -0
  43. package/cpp/Av1SequenceHeader.h +25 -0
  44. package/cpp/ByteSource.cpp +116 -0
  45. package/cpp/ByteSource.h +59 -0
  46. package/cpp/RangeSourceClient.h +40 -0
  47. package/cpp/VideoDecoder.cpp +157 -0
  48. package/cpp/VideoDecoder.h +83 -0
  49. package/cpp/WebMAlphaPlayer.cpp +1666 -0
  50. package/cpp/WebMAlphaPlayer.h +389 -0
  51. package/cpp/WebMAlphaPlayer_jni.cpp +1380 -0
  52. package/cpp/YuvToRgb.cpp +39 -0
  53. package/cpp/YuvToRgb.h +40 -0
  54. package/ios/Sources/AlphaVideo/AlphaVideoCodecs.mm +63 -0
  55. package/ios/Sources/AlphaVideo/AlphaVideoInternal.h +70 -0
  56. package/ios/Sources/AlphaVideo/AlphaVideoPlayer.mm +994 -0
  57. package/ios/Sources/AlphaVideo/AlphaVideoSource.mm +476 -0
  58. package/ios/Sources/AlphaVideo/AlphaVideoView.mm +374 -0
  59. package/ios/Sources/AlphaVideo/include/AlphaVideo.h +303 -0
  60. package/ios/Sources/AlphaVideoAV1/AlphaVideoAV1Codec.mm +16 -0
  61. package/ios/Sources/AlphaVideoAV1/include/AlphaVideoAV1.h +18 -0
  62. package/ios/Sources/AlphaVideoVPX/AlphaVideoVPXCodec.mm +16 -0
  63. package/ios/Sources/AlphaVideoVPX/include/AlphaVideoVPX.h +17 -0
  64. package/lib/commonjs/AlphaVideoView.js +187 -0
  65. package/lib/commonjs/AlphaVideoView.js.map +1 -0
  66. package/lib/commonjs/AlphaVideoViewNativeComponent.js +47 -0
  67. package/lib/commonjs/AlphaVideoViewNativeComponent.js.map +1 -0
  68. package/lib/commonjs/AlphaVideoViewNativeComponent.web.js +11 -0
  69. package/lib/commonjs/AlphaVideoViewNativeComponent.web.js.map +1 -0
  70. package/lib/commonjs/NativeAlphaVideoModule.js +10 -0
  71. package/lib/commonjs/NativeAlphaVideoModule.js.map +1 -0
  72. package/lib/commonjs/NativeAlphaVideoModule.web.js +9 -0
  73. package/lib/commonjs/NativeAlphaVideoModule.web.js.map +1 -0
  74. package/lib/commonjs/WebPlayer.js +12 -0
  75. package/lib/commonjs/WebPlayer.js.map +1 -0
  76. package/lib/commonjs/WebPlayer.web.js +92 -0
  77. package/lib/commonjs/WebPlayer.web.js.map +1 -0
  78. package/lib/commonjs/index.js +124 -0
  79. package/lib/commonjs/index.js.map +1 -0
  80. package/lib/module/AlphaVideoView.js +187 -0
  81. package/lib/module/AlphaVideoView.js.map +1 -0
  82. package/lib/module/AlphaVideoViewNativeComponent.js +47 -0
  83. package/lib/module/AlphaVideoViewNativeComponent.js.map +1 -0
  84. package/lib/module/AlphaVideoViewNativeComponent.web.js +11 -0
  85. package/lib/module/AlphaVideoViewNativeComponent.web.js.map +1 -0
  86. package/lib/module/NativeAlphaVideoModule.js +10 -0
  87. package/lib/module/NativeAlphaVideoModule.js.map +1 -0
  88. package/lib/module/NativeAlphaVideoModule.web.js +9 -0
  89. package/lib/module/NativeAlphaVideoModule.web.js.map +1 -0
  90. package/lib/module/WebPlayer.js +12 -0
  91. package/lib/module/WebPlayer.js.map +1 -0
  92. package/lib/module/WebPlayer.web.js +92 -0
  93. package/lib/module/WebPlayer.web.js.map +1 -0
  94. package/lib/module/index.js +124 -0
  95. package/lib/module/index.js.map +1 -0
  96. package/lib/typescript/react-native/src/AlphaVideoView.d.ts +189 -0
  97. package/lib/typescript/react-native/src/AlphaVideoView.d.ts.map +1 -0
  98. package/lib/typescript/react-native/src/AlphaVideoViewNativeComponent.d.ts +109 -0
  99. package/lib/typescript/react-native/src/AlphaVideoViewNativeComponent.d.ts.map +1 -0
  100. package/lib/typescript/react-native/src/AlphaVideoViewNativeComponent.web.d.ts +5 -0
  101. package/lib/typescript/react-native/src/AlphaVideoViewNativeComponent.web.d.ts.map +1 -0
  102. package/lib/typescript/react-native/src/NativeAlphaVideoModule.d.ts +21 -0
  103. package/lib/typescript/react-native/src/NativeAlphaVideoModule.d.ts.map +1 -0
  104. package/lib/typescript/react-native/src/NativeAlphaVideoModule.web.d.ts +3 -0
  105. package/lib/typescript/react-native/src/NativeAlphaVideoModule.web.d.ts.map +1 -0
  106. package/lib/typescript/react-native/src/WebPlayer.d.ts +3 -0
  107. package/lib/typescript/react-native/src/WebPlayer.d.ts.map +1 -0
  108. package/lib/typescript/react-native/src/WebPlayer.web.d.ts +9 -0
  109. package/lib/typescript/react-native/src/WebPlayer.web.d.ts.map +1 -0
  110. package/lib/typescript/react-native/src/index.d.ts +77 -0
  111. package/lib/typescript/react-native/src/index.d.ts.map +1 -0
  112. package/licenses/dav1d-COPYING.txt +23 -0
  113. package/licenses/libvpx-LICENSE.txt +31 -0
  114. package/licenses/libvpx-PATENTS.txt +23 -0
  115. package/licenses/libwebm-LICENSE.txt +30 -0
  116. package/licenses/libwebm-PATENTS.txt +23 -0
  117. package/package.json +150 -4
  118. package/prebuilt/android/arm64-v8a/lib/libdav1d.a +0 -0
  119. package/prebuilt/android/arm64-v8a/lib/libvpx.a +0 -0
  120. package/prebuilt/android/armeabi-v7a/lib/libdav1d.a +0 -0
  121. package/prebuilt/android/armeabi-v7a/lib/libvpx.a +0 -0
  122. package/prebuilt/android/x86_64/lib/libdav1d.a +0 -0
  123. package/prebuilt/android/x86_64/lib/libvpx.a +0 -0
  124. package/prebuilt/include/dav1d/common.h +94 -0
  125. package/prebuilt/include/dav1d/data.h +117 -0
  126. package/prebuilt/include/dav1d/dav1d.h +329 -0
  127. package/prebuilt/include/dav1d/headers.h +440 -0
  128. package/prebuilt/include/dav1d/picture.h +157 -0
  129. package/prebuilt/include/dav1d/version.h +50 -0
  130. package/prebuilt/include/vpx/vp8.h +136 -0
  131. package/prebuilt/include/vpx/vp8dx.h +228 -0
  132. package/prebuilt/include/vpx/vpx_codec.h +475 -0
  133. package/prebuilt/include/vpx/vpx_decoder.h +365 -0
  134. package/prebuilt/include/vpx/vpx_frame_buffer.h +83 -0
  135. package/prebuilt/include/vpx/vpx_image.h +209 -0
  136. package/prebuilt/include/vpx/vpx_integer.h +40 -0
  137. package/prebuilt/ios/lib/libdav1d.a +0 -0
  138. package/prebuilt/ios/lib/libvpx.a +0 -0
  139. package/prebuilt/ios-simulator/include/dav1d/common.h +94 -0
  140. package/prebuilt/ios-simulator/include/dav1d/data.h +117 -0
  141. package/prebuilt/ios-simulator/include/dav1d/dav1d.h +329 -0
  142. package/prebuilt/ios-simulator/include/dav1d/headers.h +440 -0
  143. package/prebuilt/ios-simulator/include/dav1d/picture.h +157 -0
  144. package/prebuilt/ios-simulator/include/dav1d/version.h +50 -0
  145. package/prebuilt/ios-simulator/include/vpx/vp8.h +136 -0
  146. package/prebuilt/ios-simulator/include/vpx/vp8dx.h +228 -0
  147. package/prebuilt/ios-simulator/include/vpx/vpx_codec.h +475 -0
  148. package/prebuilt/ios-simulator/include/vpx/vpx_decoder.h +365 -0
  149. package/prebuilt/ios-simulator/include/vpx/vpx_frame_buffer.h +83 -0
  150. package/prebuilt/ios-simulator/include/vpx/vpx_image.h +209 -0
  151. package/prebuilt/ios-simulator/include/vpx/vpx_integer.h +40 -0
  152. package/prebuilt/ios-simulator/lib/libdav1d.a +0 -0
  153. package/prebuilt/ios-simulator/lib/libvpx.a +0 -0
  154. package/react-native/android/CMakeLists.txt +14 -0
  155. package/react-native/android/build.gradle +110 -0
  156. package/react-native/android/src/main/AndroidManifest.xml +1 -0
  157. package/react-native/android/src/main/java/dev/mediacache/alphavideo/reactnative/AlphaVideoModule.java +102 -0
  158. package/react-native/android/src/main/java/dev/mediacache/alphavideo/reactnative/AlphaVideoPackage.java +50 -0
  159. package/react-native/android/src/main/java/dev/mediacache/alphavideo/reactnative/AlphaVideoViewManager.java +317 -0
  160. package/react-native/android/src/newarch/java/dev/mediacache/alphavideo/reactnative/AlphaVideoModuleSpec.java +10 -0
  161. package/react-native/android/src/newarch/java/dev/mediacache/alphavideo/reactnative/AlphaVideoViewManagerSpec.java +23 -0
  162. package/react-native/android/src/norangesource/java/dev/mediacache/alphavideo/AppDataSource.java +15 -0
  163. package/react-native/android/src/oldarch/java/dev/mediacache/alphavideo/reactnative/AlphaVideoModuleSpec.java +30 -0
  164. package/react-native/android/src/oldarch/java/dev/mediacache/alphavideo/reactnative/AlphaVideoViewManagerSpec.java +61 -0
  165. package/react-native/ios/RNAlphaVideoViewComponentView.mm +173 -0
  166. package/react-native/ios/RNAlphaVideoViewManager.mm +147 -0
  167. package/react-native/src/AlphaVideoView.tsx +329 -0
  168. package/react-native/src/AlphaVideoViewNativeComponent.ts +132 -0
  169. package/react-native/src/AlphaVideoViewNativeComponent.web.ts +5 -0
  170. package/react-native/src/NativeAlphaVideoModule.ts +22 -0
  171. package/react-native/src/NativeAlphaVideoModule.web.ts +2 -0
  172. package/react-native/src/WebPlayer.tsx +7 -0
  173. package/react-native/src/WebPlayer.web.tsx +60 -0
  174. package/react-native/src/index.tsx +144 -0
  175. package/react-native-alpha-video.podspec +99 -0
  176. package/react-native.config.js +12 -0
  177. package/scripts/build_codecs_android.sh +246 -0
  178. package/scripts/build_codecs_ios.sh +31 -0
  179. package/scripts/build_codecs_ios_device.sh +210 -0
  180. package/scripts/build_codecs_ios_simulator.sh +256 -0
  181. package/scripts/build_wasm.sh +230 -0
  182. package/scripts/build_xcframework.sh +49 -0
  183. package/scripts/fetch-prebuilt.sh +48 -0
  184. package/scripts/package_release.sh +107 -0
  185. package/scripts/publish-android.sh +85 -0
  186. package/scripts/vendor_range_source.sh +47 -0
  187. package/third_party/libwebm/AUTHORS.TXT +5 -0
  188. package/third_party/libwebm/LICENSE.TXT +30 -0
  189. package/third_party/libwebm/PATENTS.TXT +23 -0
  190. package/third_party/libwebm/README.md +7 -0
  191. package/third_party/libwebm/webm_parser/include/webm/buffer_reader.h +138 -0
  192. package/third_party/libwebm/webm_parser/include/webm/callback.h +363 -0
  193. package/third_party/libwebm/webm_parser/include/webm/dom_types.h +1781 -0
  194. package/third_party/libwebm/webm_parser/include/webm/element.h +208 -0
  195. package/third_party/libwebm/webm_parser/include/webm/file_reader.h +103 -0
  196. package/third_party/libwebm/webm_parser/include/webm/id.h +1085 -0
  197. package/third_party/libwebm/webm_parser/include/webm/istream_reader.h +99 -0
  198. package/third_party/libwebm/webm_parser/include/webm/reader.h +94 -0
  199. package/third_party/libwebm/webm_parser/include/webm/status.h +166 -0
  200. package/third_party/libwebm/webm_parser/include/webm/webm_parser.h +133 -0
  201. package/third_party/libwebm/webm_parser/src/ancestory.cc +334 -0
  202. package/third_party/libwebm/webm_parser/src/ancestory.h +83 -0
  203. package/third_party/libwebm/webm_parser/src/audio_parser.h +84 -0
  204. package/third_party/libwebm/webm_parser/src/bit_utils.cc +26 -0
  205. package/third_party/libwebm/webm_parser/src/bit_utils.h +24 -0
  206. package/third_party/libwebm/webm_parser/src/block_additions_parser.h +30 -0
  207. package/third_party/libwebm/webm_parser/src/block_group_parser.h +71 -0
  208. package/third_party/libwebm/webm_parser/src/block_header_parser.cc +76 -0
  209. package/third_party/libwebm/webm_parser/src/block_header_parser.h +65 -0
  210. package/third_party/libwebm/webm_parser/src/block_more_parser.h +32 -0
  211. package/third_party/libwebm/webm_parser/src/block_parser.cc +285 -0
  212. package/third_party/libwebm/webm_parser/src/block_parser.h +126 -0
  213. package/third_party/libwebm/webm_parser/src/bool_parser.h +95 -0
  214. package/third_party/libwebm/webm_parser/src/buffer_reader.cc +110 -0
  215. package/third_party/libwebm/webm_parser/src/byte_parser.h +144 -0
  216. package/third_party/libwebm/webm_parser/src/callback.cc +155 -0
  217. package/third_party/libwebm/webm_parser/src/chapter_atom_parser.h +42 -0
  218. package/third_party/libwebm/webm_parser/src/chapter_display_parser.h +34 -0
  219. package/third_party/libwebm/webm_parser/src/chapters_parser.h +28 -0
  220. package/third_party/libwebm/webm_parser/src/cluster_parser.h +48 -0
  221. package/third_party/libwebm/webm_parser/src/colour_parser.h +55 -0
  222. package/third_party/libwebm/webm_parser/src/content_enc_aes_settings_parser.h +32 -0
  223. package/third_party/libwebm/webm_parser/src/content_encoding_parser.h +38 -0
  224. package/third_party/libwebm/webm_parser/src/content_encodings_parser.h +30 -0
  225. package/third_party/libwebm/webm_parser/src/content_encryption_parser.h +37 -0
  226. package/third_party/libwebm/webm_parser/src/cue_point_parser.h +38 -0
  227. package/third_party/libwebm/webm_parser/src/cue_track_positions_parser.h +39 -0
  228. package/third_party/libwebm/webm_parser/src/cues_parser.h +27 -0
  229. package/third_party/libwebm/webm_parser/src/date_parser.cc +72 -0
  230. package/third_party/libwebm/webm_parser/src/date_parser.h +64 -0
  231. package/third_party/libwebm/webm_parser/src/ebml_parser.h +48 -0
  232. package/third_party/libwebm/webm_parser/src/edition_entry_parser.h +35 -0
  233. package/third_party/libwebm/webm_parser/src/element_parser.h +102 -0
  234. package/third_party/libwebm/webm_parser/src/file_reader.cc +142 -0
  235. package/third_party/libwebm/webm_parser/src/float_parser.cc +77 -0
  236. package/third_party/libwebm/webm_parser/src/float_parser.h +65 -0
  237. package/third_party/libwebm/webm_parser/src/id_element_parser.cc +49 -0
  238. package/third_party/libwebm/webm_parser/src/id_element_parser.h +56 -0
  239. package/third_party/libwebm/webm_parser/src/id_parser.cc +75 -0
  240. package/third_party/libwebm/webm_parser/src/id_parser.h +45 -0
  241. package/third_party/libwebm/webm_parser/src/info_parser.h +44 -0
  242. package/third_party/libwebm/webm_parser/src/int_parser.h +121 -0
  243. package/third_party/libwebm/webm_parser/src/istream_reader.cc +133 -0
  244. package/third_party/libwebm/webm_parser/src/master_parser.cc +298 -0
  245. package/third_party/libwebm/webm_parser/src/master_parser.h +230 -0
  246. package/third_party/libwebm/webm_parser/src/master_value_parser.h +533 -0
  247. package/third_party/libwebm/webm_parser/src/mastering_metadata_parser.h +57 -0
  248. package/third_party/libwebm/webm_parser/src/parser.h +34 -0
  249. package/third_party/libwebm/webm_parser/src/parser_utils.cc +34 -0
  250. package/third_party/libwebm/webm_parser/src/parser_utils.h +64 -0
  251. package/third_party/libwebm/webm_parser/src/projection_parser.h +40 -0
  252. package/third_party/libwebm/webm_parser/src/recursive_parser.h +93 -0
  253. package/third_party/libwebm/webm_parser/src/seek_head_parser.h +27 -0
  254. package/third_party/libwebm/webm_parser/src/seek_parser.h +37 -0
  255. package/third_party/libwebm/webm_parser/src/segment_parser.cc +86 -0
  256. package/third_party/libwebm/webm_parser/src/segment_parser.h +53 -0
  257. package/third_party/libwebm/webm_parser/src/simple_tag_parser.h +37 -0
  258. package/third_party/libwebm/webm_parser/src/size_parser.cc +56 -0
  259. package/third_party/libwebm/webm_parser/src/size_parser.h +43 -0
  260. package/third_party/libwebm/webm_parser/src/skip_callback.h +63 -0
  261. package/third_party/libwebm/webm_parser/src/skip_parser.cc +59 -0
  262. package/third_party/libwebm/webm_parser/src/skip_parser.h +35 -0
  263. package/third_party/libwebm/webm_parser/src/slices_parser.h +30 -0
  264. package/third_party/libwebm/webm_parser/src/tag_parser.h +37 -0
  265. package/third_party/libwebm/webm_parser/src/tags_parser.h +27 -0
  266. package/third_party/libwebm/webm_parser/src/targets_parser.h +35 -0
  267. package/third_party/libwebm/webm_parser/src/time_slice_parser.h +30 -0
  268. package/third_party/libwebm/webm_parser/src/track_entry_parser.h +65 -0
  269. package/third_party/libwebm/webm_parser/src/tracks_parser.h +27 -0
  270. package/third_party/libwebm/webm_parser/src/unknown_parser.cc +49 -0
  271. package/third_party/libwebm/webm_parser/src/unknown_parser.h +38 -0
  272. package/third_party/libwebm/webm_parser/src/var_int_parser.cc +71 -0
  273. package/third_party/libwebm/webm_parser/src/var_int_parser.h +56 -0
  274. package/third_party/libwebm/webm_parser/src/video_parser.h +122 -0
  275. package/third_party/libwebm/webm_parser/src/virtual_block_parser.cc +72 -0
  276. package/third_party/libwebm/webm_parser/src/virtual_block_parser.h +69 -0
  277. package/third_party/libwebm/webm_parser/src/void_parser.cc +49 -0
  278. package/third_party/libwebm/webm_parser/src/void_parser.h +41 -0
  279. package/third_party/libwebm/webm_parser/src/webm_parser.cc +282 -0
  280. package/third_party/range-source/LICENSE +21 -0
  281. package/third_party/range-source/README.md +8 -0
  282. package/third_party/range-source/include/range_source.h +395 -0
  283. 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