react-native-alpha-video 0.0.0-stage → 1.0.1

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 +908 -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 +90 -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 +1982 -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 +115 -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 +115 -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 +152 -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 +113 -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 +87 -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,978 @@
1
+ package dev.mediacache.alphavideo;
2
+
3
+ import android.media.MediaFormat;
4
+ import android.os.Handler;
5
+ import android.os.HandlerThread;
6
+ import android.util.Log;
7
+
8
+ import java.nio.ByteBuffer;
9
+ import java.util.concurrent.atomic.AtomicBoolean;
10
+ import java.util.concurrent.atomic.AtomicInteger;
11
+
12
+ /**
13
+ * Direct OES dual-stream alpha player (VP8, VP9 or AV1).
14
+ *
15
+ * Colour and alpha come from two tracks, or from one VP8 or VP9 track whose alpha is in each block's
16
+ * BlockAdditional (a standard alpha WebM); the native demuxer hands both out as stream 0 and 1.
17
+ *
18
+ * Architecture (per instructions3.json):
19
+ * - Native WebM demux → MediaCodec (Surface) → SurfaceTexture → OES texture
20
+ * - Single GL draw call composites both OES textures in fragment shader
21
+ * - NO intermediate FBO ring buffer — SurfaceTexture IS the buffer
22
+ *
23
+ * Timing model:
24
+ * - MediaCodec + SurfaceTexture control decode cadence and buffering
25
+ * - Playback clock (frame-index based) controls presentation rate
26
+ * - Both SurfaceTextures are drained at frame rate to prevent backpressure
27
+ *
28
+ * Looping protocol:
29
+ * 1. Stop feeding packets
30
+ * 2. Drain SurfaceTextures via updateTexImage()
31
+ * 3. Flush both MediaCodecs
32
+ * 4. Seek demuxer to frame 0
33
+ * 5. Reset frame index, restart feeding
34
+ */
35
+ public class BufferedDualStreamPlayer implements SurfaceTextureVideoDecoder.FrameAvailableCallback {
36
+ private static final String TAG = "DualStreamOES";
37
+
38
+ // Native demuxer handle
39
+ private long nativeDemuxerPtr = 0;
40
+ private String[] decoderNames;
41
+
42
+ // Surface-based decoders (OES texture output)
43
+ private SurfaceTextureVideoDecoder colorDecoder;
44
+ private SurfaceTextureVideoDecoder alphaDecoder;
45
+
46
+ // Video info
47
+ private int width;
48
+ private int height;
49
+ private int totalFrames;
50
+ private double frameRate;
51
+ // For a shader converting the decoders' raw YUV itself; set on load
52
+ private volatile float[] colorYuvToRgb;
53
+ private volatile float[] alphaRange;
54
+
55
+ /**
56
+ * Wall-clock rate multiplier. Volatile: written from the UI thread, read by the playback loop.
57
+ *
58
+ * Scales only how long each frame is shown. The frame index and the PTS handed to the
59
+ * decoders are in the media's own timebase and must not move, or a rate change would seek
60
+ * as well as accelerate.
61
+ */
62
+ private volatile double playbackSpeed = 1.0;
63
+ private double duration;
64
+
65
+ // State
66
+ private final AtomicBoolean isLoaded = new AtomicBoolean(false);
67
+ private final AtomicBoolean isPlaying = new AtomicBoolean(false);
68
+ private final AtomicBoolean shouldStop = new AtomicBoolean(false);
69
+ private final AtomicInteger playbackFrameIndex = new AtomicInteger(0);
70
+
71
+ // A seek the demux thread hasn't carried out yet (-1: none); a later seek replaces it
72
+ private final AtomicInteger seekRequest = new AtomicInteger(-1);
73
+ // From a seek call until the decoders are repositioned; the playback loop leaves their targets alone
74
+ private final AtomicBoolean seeking = new AtomicBoolean(false);
75
+ // Frame to show once both decoders have it, playing or paused: the first one after load or a seek (-1: none)
76
+ private final AtomicInteger presentFrame = new AtomicInteger(-1);
77
+ // Frame pairs consumeFrames() has taken; a redraw for some other reason may show presentFrame first
78
+ private final AtomicInteger consumedPairs = new AtomicInteger(0);
79
+ private volatile int consumedPairsAtPresent = 0;
80
+ // Presentation time of the last pair consumeFrames() took (-1: none since the last flush)
81
+ private volatile long consumedPtsUs = -1;
82
+ // How long a loop waits for the last frame to be drawn once playback has passed it, in case a
83
+ // decoder never outputs it
84
+ private static final long LAST_FRAME_WAIT_NS = 250_000_000L;
85
+
86
+ // Frame availability — set by onFrameAvailable, used only for initial sync
87
+ // After initial frames arrive, the playback loop renders unconditionally at frame rate
88
+ private final AtomicBoolean colorDirty = new AtomicBoolean(false);
89
+ private final AtomicBoolean alphaDirty = new AtomicBoolean(false);
90
+
91
+ // Transform matrices (updated on GL thread by consumeFrames())
92
+ private final float[] colorTransformMatrix = new float[16];
93
+ private final float[] alphaTransformMatrix = new float[16];
94
+
95
+ // Decode tracking
96
+ private volatile int decodeFrameIndex = 0;
97
+
98
+ // Threading
99
+ private HandlerThread demuxThread;
100
+ private Handler demuxHandler;
101
+ private HandlerThread playbackThread;
102
+ private Handler playbackHandler;
103
+
104
+ // GL operations executor (runs tasks on GL thread)
105
+ private GLExecutor glExecutor;
106
+
107
+ // Configuration
108
+ private boolean loop = true;
109
+ private volatile boolean verbose = false;
110
+ private RenderCallback renderCallback;
111
+ private EventCallback eventCallback;
112
+
113
+ // Performance tracking
114
+ private long lastPerfLogTime = 0;
115
+ private int framesRendered = 0;
116
+
117
+ /**
118
+ * Callback to signal the view that a frame is ready for compositing.
119
+ * The view should call consumeFrames() on the GL thread, then composite
120
+ * using the OES textures and transform matrices.
121
+ */
122
+ public interface RenderCallback {
123
+ void onRenderRequested(int frameIndex);
124
+ }
125
+
126
+ /**
127
+ * Interface for executing tasks on the GL thread.
128
+ */
129
+ public interface GLExecutor {
130
+ void executeOnGLThread(Runnable task);
131
+ }
132
+
133
+ /**
134
+ * Lifecycle event callback.
135
+ */
136
+ public interface EventCallback {
137
+ void onLoaded(int totalFrames, int width, int height, double fps, double duration);
138
+ void onError(String error);
139
+ void onLooped();
140
+ void onEnded();
141
+ }
142
+
143
+ // -------------------------------------------------------------------------
144
+ // Public API
145
+ // -------------------------------------------------------------------------
146
+
147
+ public void setLoop(boolean loop) {
148
+ this.loop = loop;
149
+ }
150
+
151
+ /** Lifecycle and performance chatter, only with verbose. Warnings and errors always log. */
152
+ private void logV(String message) {
153
+ if (verbose) Log.i(TAG, message);
154
+ }
155
+
156
+ public void setVerbose(boolean verbose) {
157
+ this.verbose = verbose;
158
+ // Propagate to decoders
159
+ SurfaceTextureVideoDecoder cd = colorDecoder;
160
+ SurfaceTextureVideoDecoder ad = alphaDecoder;
161
+ if (cd != null) cd.setVerbose(verbose);
162
+ if (ad != null) ad.setVerbose(verbose);
163
+ }
164
+
165
+ public void setRenderCallback(RenderCallback callback) {
166
+ this.renderCallback = callback;
167
+ }
168
+
169
+ public void setEventCallback(EventCallback callback) {
170
+ this.eventCallback = callback;
171
+ }
172
+
173
+ public void setGLExecutor(GLExecutor executor) {
174
+ this.glExecutor = executor;
175
+ }
176
+
177
+ /**
178
+ * Load a dual-stream WebM from bytes [offset, offset + length) of fd (length -1: to end of file).
179
+ * The demuxer duplicates fd, so the caller may close it as soon as this returns.
180
+ * MUST be called from GL thread so OES textures are created in correct EGL context.
181
+ */
182
+ public boolean loadFromFd(int fd, long offset, long length) {
183
+ if (isLoaded.get()) {
184
+ unload();
185
+ }
186
+ return load(nativeCreateDemuxer(fd, offset, length));
187
+ }
188
+
189
+ /**
190
+ * Load through a range reader, which fetches the file as it plays.
191
+ * MUST be called from GL thread so OES textures are created in correct EGL context.
192
+ */
193
+ boolean loadFromReader(SourceReader reader) {
194
+ if (isLoaded.get()) {
195
+ unload();
196
+ }
197
+ long reference = reader.retain();
198
+ if (reference == 0) return false;
199
+ try {
200
+ return load(nativeCreateDemuxerFromReader(reference));
201
+ } finally {
202
+ SourceReader.release(reference);
203
+ }
204
+ }
205
+
206
+ private boolean load(long demuxerPtr) {
207
+ try {
208
+ nativeDemuxerPtr = demuxerPtr;
209
+ if (nativeDemuxerPtr == 0) {
210
+ Log.e(TAG, "Failed to create demuxer");
211
+ return false;
212
+ }
213
+
214
+ // Get video info: [width, height, totalFrames, frameRate*1000, streamCount, codec]
215
+ int[] videoInfo = nativeGetVideoInfo(nativeDemuxerPtr);
216
+ if (videoInfo == null || videoInfo.length < 6) {
217
+ Log.e(TAG, "Failed to get video info");
218
+ nativeDestroyDemuxer(nativeDemuxerPtr);
219
+ nativeDemuxerPtr = 0;
220
+ return false;
221
+ }
222
+
223
+ width = videoInfo[0];
224
+ height = videoInfo[1];
225
+ totalFrames = videoInfo[2];
226
+ int fpsMul1000 = videoInfo[3];
227
+ int numStreams = videoInfo[4];
228
+ int codec = videoInfo[5];
229
+
230
+ String mimeType = MediaCodecDecoders.mimeFor(codec);
231
+
232
+ frameRate = fpsMul1000 / 1000.0;
233
+ duration = totalFrames / frameRate;
234
+
235
+ logV("Video: " + width + "x" + height + ", " + totalFrames + " frames, " +
236
+ String.format("%.2f", frameRate) + " fps, " + numStreams + " streams, codec=" + mimeType);
237
+
238
+ if (numStreams < 2) {
239
+ Log.e(TAG, "Not a dual-stream file");
240
+ nativeDestroyDemuxer(nativeDemuxerPtr);
241
+ nativeDemuxerPtr = 0;
242
+ return false;
243
+ }
244
+
245
+ // Create decoders with detected MIME type — OES textures are created here (must be on GL thread)
246
+ if (decoderNames == null) {
247
+ Log.e(TAG, "No decoders reserved");
248
+ nativeDestroyDemuxer(nativeDemuxerPtr);
249
+ nativeDemuxerPtr = 0;
250
+ return false;
251
+ }
252
+ colorDecoder = new SurfaceTextureVideoDecoder("color", mimeType, decoderNames[0]);
253
+ alphaDecoder = new SurfaceTextureVideoDecoder("alpha", mimeType, decoderNames[1]);
254
+ colorDecoder.setVerbose(verbose);
255
+ alphaDecoder.setVerbose(verbose);
256
+
257
+ // [colour range, colour space, alpha range, alpha space]
258
+ int[] colorInfo = nativeGetColorInfo(nativeDemuxerPtr);
259
+ colorYuvToRgb = WebMAlphaPlayerNative.nativeYuvToRgbFor(colorInfo[0], colorInfo[1], width, height);
260
+ alphaRange = MediaCodecColor.range(colorInfo[2]) == MediaFormat.COLOR_RANGE_FULL
261
+ ? new float[] {1f, 0f} : new float[] {255f / 219f, 16f / 255f};
262
+ if (!colorDecoder.configure(width, height, colorInfo[0], colorInfo[1])
263
+ || !alphaDecoder.configure(width, height, colorInfo[2], colorInfo[3])) {
264
+ Log.e(TAG, "Failed to configure decoders");
265
+ cleanup();
266
+ return false;
267
+ }
268
+
269
+ // onFrameAvailable just sets dirty flags — no FBO copy needed
270
+ colorDecoder.setFrameCallback(this);
271
+ alphaDecoder.setFrameCallback(this);
272
+
273
+ if (!colorDecoder.start() || !alphaDecoder.start()) {
274
+ Log.e(TAG, "Failed to start decoders");
275
+ cleanup();
276
+ return false;
277
+ }
278
+
279
+ // Start threads
280
+ demuxThread = new HandlerThread("DemuxThread");
281
+ demuxThread.start();
282
+ demuxHandler = new Handler(demuxThread.getLooper());
283
+
284
+ playbackThread = new HandlerThread("PlaybackThread");
285
+ playbackThread.start();
286
+ playbackHandler = new Handler(playbackThread.getLooper());
287
+
288
+ isLoaded.set(true);
289
+
290
+ // Decoding starts now, playing or not, so that frame 0 shows even when paused.
291
+ shouldStop.set(false);
292
+ playbackFrameIndex.set(0);
293
+ consumedPairsAtPresent = consumedPairs.get();
294
+ presentFrame.set(0);
295
+ demuxHandler.post(this::demuxLoop);
296
+ playbackHandler.post(this::playbackLoop);
297
+
298
+ logV("Loaded successfully (direct OES pipeline)");
299
+
300
+ if (eventCallback != null) {
301
+ eventCallback.onLoaded(totalFrames, width, height, frameRate, duration);
302
+ }
303
+
304
+ return true;
305
+
306
+ } catch (Exception e) {
307
+ Log.e(TAG, "Load error: " + e.getMessage());
308
+ cleanup();
309
+ return false;
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Start or resume playback. A clip that doesn't loop and has ended starts over.
315
+ */
316
+ public void play() {
317
+ if (!isLoaded.get() || isPlaying.get()) return;
318
+
319
+ if (!loop && playbackFrameIndex.get() >= totalFrames && seekRequest.get() < 0) {
320
+ seekToFrame(0);
321
+ }
322
+ framesRendered = 0;
323
+ lastPerfLogTime = System.nanoTime();
324
+ isPlaying.set(true);
325
+ logV("Playing");
326
+ }
327
+
328
+ /**
329
+ * Pause playback.
330
+ */
331
+ public void pause() {
332
+ isPlaying.set(false);
333
+ logV("Paused");
334
+ }
335
+
336
+ /**
337
+ * Stop playback and go back to frame 0.
338
+ */
339
+ public void stop() {
340
+ isPlaying.set(false);
341
+ seekToFrame(0);
342
+ logV("Stopped");
343
+ }
344
+
345
+ /**
346
+ * Moves to a frame (clamped to the clip), staying playing or paused; a paused player shows it.
347
+ *
348
+ * The demux thread carries it out: it flushes both decoders, seeks to the keyframe at or
349
+ * before the frame and decodes from there, discarding the frames before it unshown.
350
+ */
351
+ public void seekToFrame(int frame) {
352
+ if (!isLoaded.get() || totalFrames <= 0) return;
353
+ seeking.set(true);
354
+ seekRequest.set(Math.max(0, Math.min(frame, totalFrames - 1)));
355
+ }
356
+
357
+ /** Moves to the frame showing at {@code seconds}; see {@link #seekToFrame}. */
358
+ public void seekToTime(double seconds) {
359
+ if (!(seconds > 0)) seconds = 0;
360
+ // The epsilon keeps a time computed as frame / fps from rounding down to the frame before.
361
+ seekToFrame((int) Math.floor(seconds * frameRate + 1e-6));
362
+ }
363
+
364
+ /**
365
+ * Unload the video.
366
+ */
367
+ public void unload() {
368
+ logV("Unloading");
369
+ shouldStop.set(true);
370
+ isPlaying.set(false);
371
+ isLoaded.set(false);
372
+ cleanup();
373
+ MediaCodecDecoders.release(decoderNames);
374
+ decoderNames = null;
375
+ }
376
+
377
+ /**
378
+ * Changes how fast frames are presented, leaving the playhead where it is.
379
+ *
380
+ * Takes effect on the next frame, so this is safe to call while playing — which is the
381
+ * intended use: a button that speeds its animation up on hover must not also jump.
382
+ */
383
+ public void setPlaybackSpeed(double speed) {
384
+ playbackSpeed = speed > 0.01 ? speed : 0.01;
385
+ }
386
+
387
+ // Getters
388
+ public boolean isLoaded() { return isLoaded.get(); }
389
+ public double getPlaybackSpeed() { return playbackSpeed; }
390
+ public boolean isPlaying() { return isPlaying.get(); }
391
+ public int getWidth() { return width; }
392
+ public int getHeight() { return height; }
393
+ public int getTotalFrames() { return totalFrames; }
394
+ public double getFrameRate() { return frameRate; }
395
+ public double getDuration() { return duration; }
396
+ public int getCurrentFrame() { return playbackFrameIndex.get(); }
397
+
398
+ public SurfaceTextureVideoDecoder getColorDecoder() { return colorDecoder; }
399
+ public SurfaceTextureVideoDecoder getAlphaDecoder() { return alphaDecoder; }
400
+
401
+ /**
402
+ * Consume pending frames from both SurfaceTextures.
403
+ * MUST be called on the GL thread.
404
+ *
405
+ * Calls updateTexImage() on both decoders to consume any frame that the
406
+ * PTS-gated pollOutput() has released. After consuming, clears the release
407
+ * flags so the decoder knows it can release the next frame on the next cycle.
408
+ *
409
+ * @return true if decoders are valid and textures were updated
410
+ */
411
+ public boolean consumeFrames() {
412
+ SurfaceTextureVideoDecoder cd = colorDecoder;
413
+ SurfaceTextureVideoDecoder ad = alphaDecoder;
414
+ if (cd == null || ad == null) return false;
415
+
416
+ // Only consume when BOTH decoders have released a frame.
417
+ // This guarantees color and alpha are from the same temporal position.
418
+ // If one decoder is slightly slower, we wait until next render cycle.
419
+ if (!cd.wasFrameReleased() || !ad.wasFrameReleased()) {
420
+ // Don't clear flags — let the slow decoder catch up.
421
+ // Return false to indicate no new synchronized frame pair.
422
+ return false;
423
+ }
424
+
425
+ // A released frame may not have reached its SurfaceTexture yet; the next render latches it
426
+ boolean colorLatched = cd.latchReleasedFrame();
427
+ boolean alphaLatched = ad.latchReleasedFrame();
428
+ if (!colorLatched || !alphaLatched) return false;
429
+
430
+ // Clear release flags so the decoder output loop can release
431
+ // the next frame on the next playback cycle
432
+ cd.clearFrameReleased();
433
+ ad.clearFrameReleased();
434
+
435
+ // Get transform matrices for the vertex shader
436
+ cd.getTransformMatrix(colorTransformMatrix);
437
+ ad.getTransformMatrix(alphaTransformMatrix);
438
+
439
+ consumedPtsUs = cd.getLatchedTimestampNs() / 1000;
440
+ consumedPairs.incrementAndGet();
441
+ return true;
442
+ }
443
+ /**
444
+ * Get color OES texture ID for use in compositor shader.
445
+ */
446
+ public int getColorOesTextureId() {
447
+ SurfaceTextureVideoDecoder cd = colorDecoder;
448
+ return cd != null ? cd.getOesTextureId() : -1;
449
+ }
450
+
451
+ /**
452
+ * Get alpha OES texture ID for use in compositor shader.
453
+ */
454
+ public int getAlphaOesTextureId() {
455
+ SurfaceTextureVideoDecoder ad = alphaDecoder;
456
+ return ad != null ? ad.getOesTextureId() : -1;
457
+ }
458
+
459
+ /**
460
+ * Get color transform matrix (valid after consumeFrames()).
461
+ */
462
+ public float[] getColorTransformMatrix() {
463
+ return colorTransformMatrix;
464
+ }
465
+
466
+ /**
467
+ * Get alpha transform matrix (valid after consumeFrames()).
468
+ */
469
+ public float[] getAlphaTransformMatrix() {
470
+ return alphaTransformMatrix;
471
+ }
472
+
473
+ /** The colour stream's YUV to RGB from its tags, a column-major mat4; null before load. */
474
+ public float[] getColorYuvToRgb() {
475
+ return colorYuvToRgb;
476
+ }
477
+
478
+ /** {scale, offset} taking the alpha stream's luma to 0..1: scale * (y - offset); null before load. */
479
+ public float[] getAlphaRange() {
480
+ return alphaRange;
481
+ }
482
+
483
+ // -------------------------------------------------------------------------
484
+ // Frame availability callback (from SurfaceTextureVideoDecoder)
485
+ // -------------------------------------------------------------------------
486
+
487
+ /**
488
+ * Called when a decoder has a new frame available on its SurfaceTexture.
489
+ *
490
+ * Per the architecture spec, this ONLY sets a dirty flag and captures metadata.
491
+ * The actual updateTexImage() happens on the GL render thread via consumeFrames().
492
+ */
493
+ @Override
494
+ public void onFrameAvailable(SurfaceTextureVideoDecoder decoder, int frameIndex, long timestampNs) {
495
+ if (shouldStop.get()) return;
496
+
497
+ if (decoder == colorDecoder) {
498
+ colorDirty.set(true);
499
+ } else if (decoder == alphaDecoder) {
500
+ alphaDirty.set(true);
501
+ }
502
+ }
503
+
504
+ @Override
505
+ public void onDecoderError(SurfaceTextureVideoDecoder decoder, String error) {
506
+ Log.e(TAG, "[" + decoder.getLabel() + "] Error: " + error);
507
+ if (eventCallback != null) {
508
+ eventCallback.onError(error);
509
+ }
510
+ }
511
+
512
+ // -------------------------------------------------------------------------
513
+ // Demux loop — feeds compressed packets to MediaCodec decoders
514
+ // -------------------------------------------------------------------------
515
+
516
+ private int loopCount = 0;
517
+
518
+ private void demuxLoop() {
519
+ logV("Demux loop started");
520
+
521
+ decodeFrameIndex = 0;
522
+ loopCount = 0;
523
+
524
+ while (!shouldStop.get() && isLoaded.get()) {
525
+ if (nativeDemuxerPtr == 0 || colorDecoder == null || alphaDecoder == null) {
526
+ Log.w(TAG, "Demux loop: invalid state, exiting");
527
+ break;
528
+ }
529
+
530
+ int seekTarget = seekRequest.getAndSet(-1);
531
+ if (seekTarget >= 0) {
532
+ reposition(seekTarget);
533
+ continue;
534
+ }
535
+
536
+ // Read two packets (one color, one alpha)
537
+ Object[] packet1 = nativeReadPacket(nativeDemuxerPtr);
538
+ if (packet1 == null) {
539
+ // End of file
540
+ logV("End of file at frame " + decodeFrameIndex);
541
+ queueEndOfStream();
542
+
543
+ if (loop) {
544
+ // Flushing now would drop the frames still queued in the decoders, so wait
545
+ // until the last one is drawn (paused: until play or a seek).
546
+ waitForLastFrameDrawn();
547
+ if (seekRequest.get() >= 0) continue;
548
+
549
+ loopCount++;
550
+ logV("Loop #" + loopCount + " - handling loop transition");
551
+
552
+ // Detect stuck loop
553
+ if (decodeFrameIndex == 0) {
554
+ Log.e(TAG, "Stuck loop detected: EOF at frame 0");
555
+ if (loopCount > 5) {
556
+ Log.e(TAG, "Too many stuck loops, stopping");
557
+ if (eventCallback != null) {
558
+ eventCallback.onError("Demuxer seek failed - cannot loop");
559
+ }
560
+ break;
561
+ }
562
+ try { Thread.sleep(100); } catch (InterruptedException e) { break; }
563
+ }
564
+
565
+ handleLoop();
566
+ continue;
567
+ }
568
+ // The playback loop ends the clip once it has shown the last frame. Until then,
569
+ // or afterwards, a seek resumes reading.
570
+ while (!shouldStop.get() && isLoaded.get() && seekRequest.get() < 0) {
571
+ try { Thread.sleep(10); } catch (InterruptedException e) { break; }
572
+ }
573
+ continue;
574
+ }
575
+
576
+ Object[] packet2 = nativeReadPacket(nativeDemuxerPtr);
577
+ if (packet2 == null) {
578
+ Log.w(TAG, "Missing second packet for frame " + decodeFrameIndex);
579
+ continue;
580
+ }
581
+
582
+ // Sort into color (stream 0) and alpha (stream 1)
583
+ int stream1 = (Integer) packet1[0];
584
+ int stream2 = (Integer) packet2[0];
585
+
586
+ Object[] colorPacket;
587
+ Object[] alphaPacket;
588
+
589
+ if (stream1 == 0 && stream2 == 1) {
590
+ colorPacket = packet1;
591
+ alphaPacket = packet2;
592
+ } else if (stream1 == 1 && stream2 == 0) {
593
+ colorPacket = packet2;
594
+ alphaPacket = packet1;
595
+ } else {
596
+ Log.e(TAG, "Stream mismatch: got " + stream1 + "," + stream2 +
597
+ " for frame " + decodeFrameIndex);
598
+ continue;
599
+ }
600
+
601
+ if ((Integer) alphaPacket[2] == 0) {
602
+ // A BlockAdditional alpha stream that skips a frame can't be decoded on from here
603
+ Log.e(TAG, "No alpha for frame " + decodeFrameIndex);
604
+ if (eventCallback != null) {
605
+ eventCallback.onError("No alpha for frame " + decodeFrameIndex);
606
+ }
607
+ break;
608
+ }
609
+
610
+ ByteBuffer colorData = (ByteBuffer) colorPacket[1];
611
+ long colorPts = (Long) colorPacket[3];
612
+ ByteBuffer alphaData = (ByteBuffer) alphaPacket[1];
613
+ long alphaPts = (Long) alphaPacket[3];
614
+
615
+ if (shouldStop.get()) break;
616
+
617
+ SurfaceTextureVideoDecoder cd = colorDecoder;
618
+ SurfaceTextureVideoDecoder ad = alphaDecoder;
619
+ if (cd == null || ad == null) break;
620
+
621
+ // Queue to color decoder — retry until accepted, stopped or overtaken by a seek
622
+ // MediaCodec backpressure naturally throttles when SurfaceTexture output is full
623
+ while (!cd.queueInputPacket(colorData, colorPts, decodeFrameIndex)
624
+ && !shouldStop.get() && seekRequest.get() < 0) {
625
+ try { Thread.sleep(1); } catch (InterruptedException e) { break; }
626
+ }
627
+
628
+ // Queue to alpha decoder
629
+ while (!ad.queueInputPacket(alphaData, alphaPts, decodeFrameIndex)
630
+ && !shouldStop.get() && seekRequest.get() < 0) {
631
+ try { Thread.sleep(1); } catch (InterruptedException e) { break; }
632
+ }
633
+
634
+ decodeFrameIndex++;
635
+ }
636
+
637
+ logV("Demux loop ended");
638
+ }
639
+
640
+ /**
641
+ * Returns once playback has passed the last frame read and that frame has been drawn, or
642
+ * LAST_FRAME_WAIT_NS after passing it without; or on stop or a seek.
643
+ */
644
+ private void waitForLastFrameDrawn() {
645
+ long frameUs = (long) (1_000_000.0 / frameRate);
646
+ // Half a frame of slack, as the playback loop's targets have
647
+ long lastFramePtsUs = (decodeFrameIndex - 1) * frameUs - frameUs / 2;
648
+ long passedAtNs = 0;
649
+ while (!shouldStop.get() && isLoaded.get() && seekRequest.get() < 0) {
650
+ boolean passed = presentFrame.get() < 0 && playbackFrameIndex.get() >= decodeFrameIndex;
651
+ if (passed && passedAtNs == 0) passedAtNs = System.nanoTime();
652
+ boolean drawn = consumedPtsUs >= lastFramePtsUs;
653
+ if (passed && (drawn || System.nanoTime() - passedAtNs > LAST_FRAME_WAIT_NS)) return;
654
+ try { Thread.sleep(5); } catch (InterruptedException e) { return; }
655
+ }
656
+ }
657
+
658
+ /** Lets both decoders output their last frames; the next loop or seek flushes them. */
659
+ private void queueEndOfStream() {
660
+ for (SurfaceTextureVideoDecoder decoder : new SurfaceTextureVideoDecoder[] {colorDecoder, alphaDecoder}) {
661
+ while (decoder != null && !decoder.queueEndOfStream() && !shouldStop.get() && seekRequest.get() < 0) {
662
+ try { Thread.sleep(1); } catch (InterruptedException e) { return; }
663
+ }
664
+ }
665
+ }
666
+
667
+ /**
668
+ * Handle loop transition per spec:
669
+ * 1. Drain SurfaceTextures via updateTexImage()
670
+ * 2. Flush both MediaCodecs
671
+ * 3. Seek demuxer to frame 0
672
+ * 4. Reset state and restart
673
+ */
674
+ private void handleLoop() {
675
+ logV("Loop transition starting...");
676
+
677
+ if (shouldStop.get()) return;
678
+
679
+ drainAndFlushDecoders();
680
+ // Hold the new pass until the playback loop asks for frame 0; a paused view keeps its frame.
681
+ SurfaceTextureVideoDecoder cd = colorDecoder;
682
+ SurfaceTextureVideoDecoder ad = alphaDecoder;
683
+ if (cd != null) cd.setTargetPresentationTimeUs(-1);
684
+ if (ad != null) ad.setTargetPresentationTimeUs(-1);
685
+
686
+ // Step 3: Seek demuxer to start
687
+ logV("Seeking demuxer to start...");
688
+ if (nativeDemuxerPtr != 0) {
689
+ nativeSeekToStart(nativeDemuxerPtr);
690
+ }
691
+
692
+ // Step 4: Reset state
693
+ colorDirty.set(false);
694
+ alphaDirty.set(false);
695
+ decodeFrameIndex = 0;
696
+ playbackFrameIndex.set(0);
697
+
698
+ // Reset perf counters
699
+ framesRendered = 0;
700
+ lastPerfLogTime = System.nanoTime();
701
+
702
+ if (eventCallback != null) {
703
+ eventCallback.onLooped();
704
+ }
705
+
706
+ logV("Loop transition complete - resuming from frame 0");
707
+ }
708
+
709
+ /**
710
+ * Restarts decoding at {@code frame}, from the keyframe at or before it. The decoders drop
711
+ * the frames before it, and the playback loop shows it as soon as both have it.
712
+ */
713
+ private void reposition(int frame) {
714
+ seeking.set(true);
715
+ logV("Seeking to frame " + frame);
716
+
717
+ drainAndFlushDecoders();
718
+
719
+ long frameUs = (long) (1_000_000.0 / frameRate);
720
+ // Half a frame either side absorbs the container's rounding of timestamps (WebM: 1 ms).
721
+ if (nativeDemuxerPtr != 0) {
722
+ nativeSeekToTime(nativeDemuxerPtr, frame * frameUs + frameUs / 2);
723
+ }
724
+ SurfaceTextureVideoDecoder cd = colorDecoder;
725
+ SurfaceTextureVideoDecoder ad = alphaDecoder;
726
+ if (cd != null) cd.setDropBeforeUs(frame * frameUs - frameUs / 2);
727
+ if (ad != null) ad.setDropBeforeUs(frame * frameUs - frameUs / 2);
728
+
729
+ colorDirty.set(false);
730
+ alphaDirty.set(false);
731
+ decodeFrameIndex = frame;
732
+ playbackFrameIndex.set(frame);
733
+ consumedPairsAtPresent = consumedPairs.get();
734
+ presentFrame.set(frame);
735
+ // A seek made meanwhile goes next, and the playback loop waits for that one instead.
736
+ if (seekRequest.get() < 0) seeking.set(false);
737
+ }
738
+
739
+ /** Loop and seek steps 1–2: drain the SurfaceTextures, then flush both decoders. */
740
+ private void drainAndFlushDecoders() {
741
+ // Step 1: Drain SurfaceTextures to prevent stale frames
742
+ // Post to GL thread and wait for completion
743
+ if (glExecutor != null) {
744
+ final Object drainLock = new Object();
745
+ final AtomicBoolean drained = new AtomicBoolean(false);
746
+
747
+ glExecutor.executeOnGLThread(() -> {
748
+ SurfaceTextureVideoDecoder cd = colorDecoder;
749
+ SurfaceTextureVideoDecoder ad = alphaDecoder;
750
+ if (cd != null && cd.hasNewFrame()) {
751
+ cd.updateTexImage();
752
+ }
753
+ if (ad != null && ad.hasNewFrame()) {
754
+ ad.updateTexImage();
755
+ }
756
+ synchronized (drainLock) {
757
+ drained.set(true);
758
+ drainLock.notifyAll();
759
+ }
760
+ });
761
+
762
+ // Wait for drain to complete (with timeout)
763
+ synchronized (drainLock) {
764
+ long deadline = System.currentTimeMillis() + 500;
765
+ while (!drained.get() && System.currentTimeMillis() < deadline) {
766
+ try { drainLock.wait(50); } catch (InterruptedException e) { break; }
767
+ }
768
+ }
769
+ }
770
+
771
+ // Step 2: Flush both decoders (resets VP9 reference frames and timestamps)
772
+ logV("Flushing decoders...");
773
+ if (colorDecoder != null) colorDecoder.flush();
774
+ if (alphaDecoder != null) alphaDecoder.flush();
775
+ consumedPtsUs = -1;
776
+ }
777
+
778
+ // -------------------------------------------------------------------------
779
+ // Playback loop — triggers rendering at frame rate
780
+ // -------------------------------------------------------------------------
781
+
782
+ private void playbackLoop() {
783
+ logV("Playback loop started");
784
+
785
+ // Media timebase — one frame's worth of presentation time, whatever the playback rate.
786
+ long frameDurationUs = (long)(1_000_000.0 / frameRate);
787
+ long nextFrameTimeNs = System.nanoTime();
788
+
789
+ while (!shouldStop.get() && isLoaded.get()) {
790
+ SurfaceTextureVideoDecoder cd = colorDecoder;
791
+ SurfaceTextureVideoDecoder ad = alphaDecoder;
792
+ if (cd == null || ad == null) break;
793
+
794
+ // Read per iteration so a mid-playback rate change takes effect on the next frame
795
+ // rather than at the next load.
796
+ double speed = playbackSpeed;
797
+ if (speed <= 0.01) speed = 0.01;
798
+ long frameDurationNs = (long)(1_000_000_000.0 / (frameRate * speed));
799
+
800
+ if (seeking.get()) {
801
+ try { Thread.sleep(2); } catch (InterruptedException e) { break; }
802
+ continue;
803
+ }
804
+
805
+ // The first frame after load or a seek: hold both decoders at it until each has
806
+ // released it, render until the view has drawn it, and start the clock from there.
807
+ int present = presentFrame.get();
808
+ if (present >= 0) {
809
+ long presentPtsUs = present * frameDurationUs + frameDurationUs / 2;
810
+ cd.setTargetPresentationTimeUs(presentPtsUs);
811
+ ad.setTargetPresentationTimeUs(presentPtsUs);
812
+ boolean drawn = consumedPairs.get() != consumedPairsAtPresent;
813
+ if (drawn && presentFrame.compareAndSet(present, -1)) {
814
+ playbackFrameIndex.set(present + 1);
815
+ nextFrameTimeNs = System.nanoTime() + frameDurationNs;
816
+ continue;
817
+ }
818
+ if (!drawn && cd.wasFrameReleased() && ad.wasFrameReleased() && renderCallback != null) {
819
+ renderCallback.onRenderRequested(present);
820
+ }
821
+ try { Thread.sleep(2); } catch (InterruptedException e) { break; }
822
+ continue;
823
+ }
824
+
825
+ if (!isPlaying.get()) {
826
+ try { Thread.sleep(16); } catch (InterruptedException e) { break; }
827
+ nextFrameTimeNs = System.nanoTime();
828
+ continue;
829
+ }
830
+
831
+ long now = System.nanoTime();
832
+
833
+ if (now >= nextFrameTimeNs) {
834
+ int frameIdx = playbackFrameIndex.get();
835
+
836
+ if (!loop && frameIdx >= totalFrames) {
837
+ isPlaying.set(false);
838
+ if (eventCallback != null) {
839
+ eventCallback.onEnded();
840
+ }
841
+ continue;
842
+ }
843
+
844
+ // Advance the target PTS on BOTH decoders so they release exactly
845
+ // this frame's data to their SurfaceTextures. Both decoders get the
846
+ // same target, ensuring color and alpha stay synchronized. Half a frame
847
+ // of slack covers the container rounding a timestamp up.
848
+ long targetPtsUs = frameIdx * frameDurationUs + frameDurationUs / 2;
849
+ cd.setTargetPresentationTimeUs(targetPtsUs);
850
+ ad.setTargetPresentationTimeUs(targetPtsUs);
851
+
852
+ // Request render — the view will call consumeFrames() which calls
853
+ // updateTexImage() to grab the frame we just allowed to be released
854
+ if (renderCallback != null) {
855
+ renderCallback.onRenderRequested(frameIdx);
856
+ }
857
+
858
+ // A seek or loop meanwhile has moved the playhead; keep it.
859
+ playbackFrameIndex.compareAndSet(frameIdx, frameIdx + 1);
860
+ framesRendered++;
861
+ nextFrameTimeNs += frameDurationNs;
862
+
863
+ // Don't fall too far behind
864
+ if (nextFrameTimeNs < now - frameDurationNs * 3) {
865
+ nextFrameTimeNs = now;
866
+ }
867
+ } else {
868
+ // Wait for next frame time
869
+ long sleepMs = (nextFrameTimeNs - now) / 1_000_000;
870
+ if (sleepMs > 0) {
871
+ try { Thread.sleep(Math.min(sleepMs, 16)); } catch (InterruptedException e) { break; }
872
+ }
873
+ }
874
+
875
+ // Performance logging every 2 seconds (verbose only)
876
+ if (verbose && now - lastPerfLogTime >= 2_000_000_000L) {
877
+ double elapsed = (now - lastPerfLogTime) / 1_000_000_000.0;
878
+ double fps = framesRendered / elapsed;
879
+
880
+ logV(String.format("PERF: fps=%.1f (target=%.1f), rendered=%d, playbackFrame=%d",
881
+ fps, frameRate, framesRendered, playbackFrameIndex.get()));
882
+
883
+ framesRendered = 0;
884
+ lastPerfLogTime = now;
885
+ }
886
+ }
887
+
888
+ logV("Playback loop ended");
889
+ }
890
+
891
+ // -------------------------------------------------------------------------
892
+ // Cleanup
893
+ // -------------------------------------------------------------------------
894
+
895
+ private void cleanup() {
896
+ logV("Starting cleanup...");
897
+
898
+ shouldStop.set(true);
899
+
900
+ // Stop demux thread
901
+ if (demuxThread != null) {
902
+ demuxThread.quitSafely();
903
+ try { demuxThread.join(500); } catch (InterruptedException e) {}
904
+ demuxThread = null;
905
+ demuxHandler = null;
906
+ }
907
+
908
+ // Stop playback thread
909
+ if (playbackThread != null) {
910
+ playbackThread.quitSafely();
911
+ try { playbackThread.join(500); } catch (InterruptedException e) {}
912
+ playbackThread = null;
913
+ playbackHandler = null;
914
+ }
915
+
916
+ // Release decoders
917
+ if (colorDecoder != null) {
918
+ colorDecoder.release();
919
+ colorDecoder = null;
920
+ }
921
+ if (alphaDecoder != null) {
922
+ alphaDecoder.release();
923
+ alphaDecoder = null;
924
+ }
925
+
926
+ // Destroy native demuxer
927
+ if (nativeDemuxerPtr != 0) {
928
+ nativeDestroyDemuxer(nativeDemuxerPtr);
929
+ nativeDemuxerPtr = 0;
930
+ }
931
+
932
+ playbackFrameIndex.set(0);
933
+ colorDirty.set(false);
934
+ alphaDirty.set(false);
935
+
936
+ logV("Cleanup complete");
937
+ }
938
+
939
+ // -------------------------------------------------------------------------
940
+ // Native methods
941
+ // -------------------------------------------------------------------------
942
+
943
+ private static native long nativeCreateDemuxer(int fd, long offset, long length);
944
+ private static native long nativeCreateDemuxerFromReader(long reader);
945
+ private static native void nativeDestroyDemuxer(long ptr);
946
+ private static native int[] nativeGetVideoInfo(long ptr);
947
+ private static native int[] nativeGetColorInfo(long ptr);
948
+ private static native Object[] nativeReadPacket(long ptr);
949
+ private static native void nativeSeekToStart(long ptr);
950
+ private static native void nativeSeekToTime(long ptr, long timeUs);
951
+
952
+ public static final int TRACK_VP8 = 0;
953
+ public static final int TRACK_VP9 = 1;
954
+ public static final int TRACK_AV1 = 2;
955
+
956
+ /**
957
+ * The colour and alpha bitstreams in bytes [offset, offset + length) of fd, as pairs of codec
958
+ * ({@link #TRACK_VP8}, {@link #TRACK_VP9} or {@link #TRACK_AV1}) and profile. They come from
959
+ * 2 or more VP8/VP9/AV1 tracks, or from one VP8 or VP9 track with its alpha in BlockAdditional
960
+ * (a standard alpha WebM). Null for any other file.
961
+ */
962
+ public static native int[] nativeColorAlphaTracks(int fd, long offset, long length);
963
+
964
+ /** The same for a {@link SourceReader} reference; blocks while the header is fetched. */
965
+ static native int[] nativeColorAlphaTracksReader(long reader);
966
+
967
+ /**
968
+ * The decoders to load with, colour then alpha, reserved by {@link MediaCodecDecoders#reserve}.
969
+ * Call before loading; the player gives them back in {@link #unload}.
970
+ */
971
+ void useDecoders(String[] names) {
972
+ decoderNames = names;
973
+ }
974
+
975
+ static {
976
+ System.loadLibrary("alphavideo");
977
+ }
978
+ }