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,1982 @@
1
+ package dev.mediacache.alphavideo;
2
+
3
+ import android.content.Context;
4
+ import android.content.res.AssetFileDescriptor;
5
+ import android.graphics.SurfaceTexture;
6
+ import android.net.Uri;
7
+ import android.opengl.EGL14;
8
+ import android.opengl.EGLConfig;
9
+ import android.opengl.EGLContext;
10
+ import android.opengl.EGLDisplay;
11
+ import android.opengl.EGLExt;
12
+ import android.opengl.EGLSurface;
13
+ import android.opengl.GLES11Ext;
14
+ import android.opengl.GLES20;
15
+ import android.util.AttributeSet;
16
+ import android.util.Log;
17
+ import android.view.TextureView;
18
+
19
+ import java.io.File;
20
+ import java.io.IOException;
21
+ import java.nio.ByteBuffer;
22
+ import java.nio.ByteOrder;
23
+ import java.nio.FloatBuffer;
24
+ import java.util.Collections;
25
+ import java.util.LinkedHashMap;
26
+ import java.util.Map;
27
+ import java.util.concurrent.ExecutorService;
28
+ import java.util.concurrent.Executors;
29
+ import java.util.concurrent.Future;
30
+ import java.util.concurrent.RejectedExecutionException;
31
+ import java.util.concurrent.atomic.AtomicBoolean;
32
+
33
+ /**
34
+ * TextureView-based WebM player that integrates properly with React Native's view hierarchy.
35
+ * Unlike GLSurfaceView, TextureView:
36
+ * - Respects z-ordering and stacking
37
+ * - Works correctly inside ScrollView
38
+ * - Supports proper clipping
39
+ * - Participates in the normal view hierarchy
40
+ *
41
+ * Uses a dedicated GL render thread for OpenGL ES operations.
42
+ */
43
+ public class AlphaVideoView extends TextureView implements
44
+ TextureView.SurfaceTextureListener,
45
+ WebMAlphaPlayerNative.PlayerListener {
46
+
47
+ private static final String TAG = "AlphaVideoView";
48
+
49
+ // Global decode queue - serializes non-stream decoding across all player instances
50
+ // This prevents CPU saturation and crashes when many players are created simultaneously
51
+ private static ExecutorService globalDecodeQueue;
52
+ private static final Object globalDecodeQueueLock = new Object();
53
+ private static int globalDecodeQueueRefCount = 0;
54
+
55
+ // C++ player handle - volatile for thread visibility
56
+ private volatile long playerId = 0;
57
+ private volatile boolean isReleased = false;
58
+
59
+ // Dual-stream player — direct OES texture compositing (no FBO ring buffer)
60
+ private BufferedDualStreamPlayer bufferedPlayer;
61
+ private volatile boolean isDualStreamMode = false;
62
+
63
+ // EGL/OpenGL resources (managed on render thread)
64
+ private EGLDisplay eglDisplay;
65
+ private EGLContext eglContext;
66
+ private EGLSurface eglSurface;
67
+ private EGLConfig eglConfig;
68
+
69
+ // OpenGL resources
70
+ private int programId; // YUVA shader (ByteBuffer mode)
71
+ private int oesProgramId; // OES shader (Surface mode - GPU accelerated)
72
+ // Surface mode converting raw YUV itself: the driver may convert software decoders' buffers as
73
+ // BT.601 whatever their dataspace (Adreno with c2.android.av1-dav1d). 0 without GL_EXT_YUV_target.
74
+ private int yuvOesProgramId;
75
+ private int glesVersion;
76
+ private int yTextureId, uTextureId, vTextureId, aTextureId;
77
+ private FloatBuffer vertexBuffer;
78
+ private FloatBuffer oesVertexBuffer; // Vertex buffer with transform matrix support
79
+ private int currentWidth, currentHeight;
80
+
81
+ // OES texture transformation matrices (from SurfaceTexture)
82
+ // The YUVA shader's YUV to RGB, set from the clip on load. Until then, limited-range BT.601.
83
+ private volatile float[] yuvToRgb = {
84
+ 1.164384f, 1.164384f, 1.164384f, 0f,
85
+ 0f, -0.391762f, 2.017232f, 0f,
86
+ 1.596027f, -0.812968f, 0f, 0f,
87
+ -0.874202f, 0.531668f, -1.085631f, 1f,
88
+ };
89
+ private final float[] colorTransformMatrix = new float[16];
90
+ private final float[] alphaTransformMatrix = new float[16];
91
+
92
+ // Surface state
93
+ private SurfaceTexture surfaceTexture;
94
+ private final AtomicBoolean surfaceAvailable = new AtomicBoolean(false);
95
+ private boolean glReady = false; // Main thread: EGL is set up for the current surface
96
+ private int surfaceWidth, surfaceHeight;
97
+
98
+ // Current frame data (protected by frameDataLock)
99
+ private final Object frameDataLock = new Object();
100
+ private volatile ByteBuffer pendingY, pendingU, pendingV, pendingA;
101
+ private volatile int pendingWidth, pendingHeight;
102
+ private volatile boolean hasNewFrame = false;
103
+
104
+ // Reusable buffers to avoid allocations during playback
105
+ private ByteBuffer reuseY, reuseU, reuseV, reuseA;
106
+
107
+ // Configuration
108
+ private boolean autoPlay = true;
109
+ // Kept so that a source loading after the paused prop was set doesn't start playing
110
+ private boolean paused = false;
111
+ private boolean loop = true;
112
+ private double playbackSpeed = 1.0;
113
+ // Reduced defaults for better performance with multiple simultaneous players
114
+ // 2 decode threads is sufficient for most glow videos (60-360px, 60-120 frames)
115
+ // 90 frame cache is enough for typical 60-90 frame glow loops
116
+ private int decodeThreads = 2;
117
+ private int cacheSize = 90;
118
+
119
+ // Playback mode:
120
+ // - "preload": decode all frames upfront, then play
121
+ // - "stream" (default): decode on-demand with sliding window cache
122
+ // - "streamAndPreload": start playing immediately, cache all frames for smooth looping
123
+ public static final String MODE_PRELOAD = "preload";
124
+ public static final String MODE_STREAM = "stream";
125
+ public static final String MODE_STREAM_AND_PRELOAD = "streamAndPreload";
126
+
127
+ private String mode = MODE_STREAM;
128
+
129
+ // Decoder for files with colour and alpha as separate VP8/VP9/AV1 bitstreams; see setDecoder()
130
+ public static final String DECODER_AUTO = "auto";
131
+ public static final String DECODER_MEDIACODEC = "mediacodec";
132
+ public static final String DECODER_MEDIACODEC_HW = "mediacodec-hw";
133
+ public static final String DECODER_MEDIACODEC_SW = "mediacodec-sw";
134
+ public static final String DECODER_SOFTWARE = "software";
135
+
136
+ private String decoder = DECODER_AUTO;
137
+ // Why "auto" left MediaCodec for the C++ core on this load, for a NO_DECODER error; else null
138
+ private String softwareFallbackReason;
139
+
140
+ /**
141
+ * {@link PlayerCallback#onError}'s code when the file needs a software decoder that isn't
142
+ * installed: add {@code dev.mediacache:alpha-video-vpx} (VP8, VP9) or {@code alpha-video-av1} (AV1),
143
+ * as the message says. See {@link AlphaVideoCodecs}.
144
+ */
145
+ public static final String ERROR_NO_DECODER = "NO_DECODER";
146
+ /**
147
+ * {@link PlayerCallback#onError}'s code for a data source ({@link #setDataSource}) without the
148
+ * range-source add-on ({@code dev.mediacache:range-source}), which reads them.
149
+ */
150
+ public static final String ERROR_NO_SOURCE_MODULE = "NO_SOURCE_MODULE";
151
+ // A URL played without the add-on is warned of once per process
152
+ private static final AtomicBoolean warnedWithoutAddOn = new AtomicBoolean(false);
153
+ private int streamPrefetchAhead = 8; // Frames to decode ahead of playback
154
+
155
+ // Use global decode queue for preload/streamAndPreload modes to prevent CPU saturation
156
+ // When true (default): decoding is serialized across all players
157
+ // When false: each player decodes independently (can cause freezes with many players)
158
+ private boolean useDecodeQueue = true;
159
+
160
+ // Verbose per-frame logging — when false, only lifecycle/perf logs are emitted
161
+ private boolean verbose = false;
162
+
163
+ // Render thread - handles all GL operations
164
+ private ExecutorService renderThread;
165
+ private final AtomicBoolean renderPending = new AtomicBoolean(false);
166
+
167
+ // Native loading executor
168
+ private ExecutorService nativeLoadExecutor;
169
+ private final Object nativeLoadLock = new Object();
170
+ private Future<?> currentLoadTask;
171
+ private volatile Future<?> softReleaseTask; // Track soft release for synchronization
172
+
173
+ // Opening a reader and probing through it wait on the network, so not on the main thread, and
174
+ // never behind a decode.
175
+ private static final ExecutorService probeExecutor = Executors.newCachedThreadPool(r -> {
176
+ Thread t = new Thread(r, "WebMProbe");
177
+ t.setDaemon(true);
178
+ return t;
179
+ });
180
+
181
+ // Where what a reader fetches is kept: cacheDir/alpha-video on disk, or the memory store's name
182
+ private static final String STORE_NAME = "alpha-video";
183
+ public static final long DEFAULT_MAX_STORAGE_BYTES = 512L * 1024 * 1024;
184
+ public static final String STORAGE_AUTO = "auto";
185
+ public static final String STORAGE_DISK = "disk";
186
+ public static final String STORAGE_MEMORY = "memory";
187
+ public static final String STORAGE_NONE = "none";
188
+
189
+ // Source as given (may be remote), and the source to load once a surface exists and again
190
+ // after reattach. Main thread only.
191
+ private String sourceUri = null;
192
+ private String localSource = null;
193
+ // Set when localSource is read through a range reader (a URL or a data source)
194
+ private SourceReader reader = null;
195
+ private String cacheKey = null;
196
+ private Map<String, String> headers = Collections.emptyMap();
197
+ private String dataSource = null;
198
+ private String storage = STORAGE_AUTO;
199
+ private long maxStorageBytes = DEFAULT_MAX_STORAGE_BYTES;
200
+ // Bumped on every setSource so a slow open can't load over a newer source.
201
+ private int sourceGeneration = 0;
202
+
203
+ // Callbacks
204
+ private PlayerCallback callback;
205
+
206
+ /** Called on the main thread. */
207
+ public interface PlayerCallback {
208
+ /**
209
+ * @param decoder the engine and decoders playing the file, e.g. "software (libvpx)" or
210
+ * "mediacodec-hw (c2.qti.vp9.decoder)"
211
+ */
212
+ void onLoad(int totalFrames, int width, int height, String decoder);
213
+ /** @param code {@link #ERROR_NO_DECODER}, or null */
214
+ void onError(String error, String code);
215
+ void onEnd();
216
+ void onPreloadComplete(int frames, double timeMs);
217
+ }
218
+
219
+ public AlphaVideoView(Context context) {
220
+ super(context);
221
+ init();
222
+ }
223
+
224
+ public AlphaVideoView(Context context, AttributeSet attrs) {
225
+ super(context, attrs);
226
+ init();
227
+ }
228
+
229
+ private void init() {
230
+ setSurfaceTextureListener(this);
231
+ setOpaque(false); // Enable transparency
232
+
233
+ // Create executors lazily - only render thread is created now
234
+ // Native player creation is deferred to onSurfaceTextureAvailable
235
+ // to prevent resource flooding when many views are created at once
236
+ logV("TextureView initialized (player creation deferred)");
237
+ }
238
+
239
+ /**
240
+ * Initialize the native player - called when surface is available
241
+ * This prevents resource flooding when many views are created but not yet visible
242
+ */
243
+ private void initializePlayer() {
244
+ if (playerId != 0) {
245
+ logV("Player already initialized: " + playerId);
246
+ return;
247
+ }
248
+
249
+ // Create executors if not already created
250
+ if (renderThread == null || renderThread.isShutdown()) {
251
+ renderThread = createRenderThread();
252
+ }
253
+ if (nativeLoadExecutor == null || nativeLoadExecutor.isShutdown()) {
254
+ nativeLoadExecutor = createLoadExecutor();
255
+ }
256
+
257
+ // Create C++ player
258
+ // preloadAllFrames should be false for stream mode, true for preload/streamAndPreload
259
+ boolean preloadAllFrames = !MODE_STREAM.equals(mode);
260
+ logV("Creating player with mode=" + mode + ", preloadAllFrames=" + preloadAllFrames +
261
+ ", streamPrefetchAhead=" + streamPrefetchAhead);
262
+ playerId = WebMAlphaPlayerNative.nativeCreate(
263
+ decodeThreads, cacheSize, preloadAllFrames, loop, playbackSpeed,
264
+ mode, streamPrefetchAhead
265
+ );
266
+ WebMAlphaPlayerNative.nativeSetListener(playerId, this);
267
+ WebMAlphaPlayerNative.nativeSetVerbose(playerId, verbose);
268
+ isReleased = false;
269
+
270
+ logV("Created TextureView player: " + playerId +
271
+ ", " + WebMAlphaPlayerNative.nativeGetLibraryVersions());
272
+ }
273
+
274
+ private ExecutorService createRenderThread() {
275
+ return Executors.newSingleThreadExecutor(r -> {
276
+ Thread t = new Thread(r, "WebMGLRender");
277
+ t.setPriority(Thread.MAX_PRIORITY);
278
+ return t;
279
+ });
280
+ }
281
+
282
+ private ExecutorService createLoadExecutor() {
283
+ return Executors.newSingleThreadExecutor(r -> {
284
+ Thread t = new Thread(r, "WebMAlphaNativeLoad");
285
+ t.setPriority(Thread.NORM_PRIORITY - 1);
286
+ return t;
287
+ });
288
+ }
289
+
290
+ /** Main thread only. */
291
+ private void reportError(String error) {
292
+ reportError(error, null);
293
+ }
294
+
295
+ private void reportError(String error, String code) {
296
+ Log.e(TAG, error);
297
+ if (callback != null && !isReleased) {
298
+ callback.onError(error, code);
299
+ }
300
+ }
301
+
302
+ /** Lifecycle chatter, only with the verbose prop. Warnings and errors always log. */
303
+ private void logV(String message) {
304
+ if (verbose) Log.i(TAG, message);
305
+ }
306
+
307
+ //------------------------------------------------------------------------------
308
+ // Public API
309
+ //------------------------------------------------------------------------------
310
+
311
+ public void setCallback(PlayerCallback callback) {
312
+ this.callback = callback;
313
+ }
314
+
315
+ public void setAutoPlay(boolean autoPlay) {
316
+ this.autoPlay = autoPlay;
317
+ }
318
+
319
+ public void setLoop(boolean loop) {
320
+ this.loop = loop;
321
+ }
322
+
323
+ /**
324
+ * Sets the playback rate, live.
325
+ *
326
+ * Stored as well as applied, because this arrives as a React prop and props are set before
327
+ * a surface exists — without keeping it, a rate given at mount would be lost and only a
328
+ * later change would ever be seen.
329
+ *
330
+ * Both backends are covered: single-stream files play through the C++ player, dual-stream
331
+ * ones through {@link BufferedDualStreamPlayer}, and which one is in use is decided at load
332
+ * time from the file itself. Neither moves the playhead when the rate changes.
333
+ */
334
+ public void setPlaybackSpeed(double playbackSpeed) {
335
+ this.playbackSpeed = playbackSpeed > 0.01 ? playbackSpeed : 0.01;
336
+
337
+ if (isDualStreamMode && bufferedPlayer != null) {
338
+ bufferedPlayer.setPlaybackSpeed(this.playbackSpeed);
339
+ } else if (playerId != 0 && !isReleased) {
340
+ WebMAlphaPlayerNative.nativeSetPlaybackSpeed(playerId, this.playbackSpeed);
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Set the playback mode.
346
+ * - "preload": decode all frames upfront, then play
347
+ * - "stream" (default): decode on-demand with sliding window cache
348
+ * - "streamAndPreload": start playing immediately, cache all frames for smooth looping
349
+ * Must be called before player is initialized (before surface is available)
350
+ */
351
+ public void setMode(String mode) {
352
+ String oldMode = this.mode;
353
+ if (MODE_PRELOAD.equals(mode) || MODE_STREAM_AND_PRELOAD.equals(mode)) {
354
+ this.mode = mode;
355
+ } else {
356
+ this.mode = MODE_STREAM; // Default
357
+ }
358
+ logV("setMode called: " + oldMode + " -> " + this.mode + " (playerId=" + playerId + ")");
359
+ }
360
+
361
+ /**
362
+ * Sets the decoder for the next load:
363
+ * - "auto" (default): MediaCodec hardware decoders that list every track's profile, else
364
+ * MediaCodec's software decoders, else the C++ core
365
+ * - "mediacodec": per track, a hardware decoder if one lists the profile and has an instance
366
+ * free, else a software one
367
+ * - "mediacodec-hw" / "mediacodec-sw": hardware / software decoders only, for both tracks
368
+ * - "software": the C++ core
369
+ * A forced engine never falls back: a file it can't play reports onError. MediaCodec plays
370
+ * only files with colour and alpha as separate VP8/VP9/AV1 bitstreams. The core decodes with
371
+ * the codec modules installed ({@link AlphaVideoCodecs}): VP8 and 8-bit VP9 with alpha-video-vpx, 8- to
372
+ * 12-bit AV1 with alpha-video-av1. A file needing one that's missing reports
373
+ * {@link #ERROR_NO_DECODER}.
374
+ */
375
+ public void setDecoder(String decoder) {
376
+ switch (decoder == null ? DECODER_AUTO : decoder) {
377
+ case DECODER_MEDIACODEC:
378
+ case DECODER_MEDIACODEC_HW:
379
+ case DECODER_MEDIACODEC_SW:
380
+ case DECODER_SOFTWARE:
381
+ this.decoder = decoder;
382
+ break;
383
+ default:
384
+ this.decoder = DECODER_AUTO;
385
+ }
386
+ }
387
+
388
+ /**
389
+ * @deprecated Use setMode() instead
390
+ */
391
+ public void setStream(boolean stream) {
392
+ this.mode = stream ? MODE_STREAM : MODE_PRELOAD;
393
+ }
394
+
395
+ /**
396
+ * Set how many frames to decode ahead of the shown one when streaming. Stream mode keeps
397
+ * only these, so this is also its memory: each is a decoded YUVA frame, about 2.5 bytes a pixel.
398
+ * Default: 8 frames (about 0.27 seconds at 30fps)
399
+ */
400
+ public void setStreamPrefetchAhead(int frames) {
401
+ this.streamPrefetchAhead = frames > 0 ? frames : 8;
402
+ }
403
+
404
+ /**
405
+ * Enable/disable global decode queue for preload/streamAndPreload modes.
406
+ * When true (default): decoding is serialized across all players, preventing CPU saturation
407
+ * When false: each player decodes independently (can freeze with many)
408
+ * Has no effect when mode='stream'.
409
+ */
410
+ public void setUseDecodeQueue(boolean use) {
411
+ this.useDecodeQueue = use;
412
+ }
413
+
414
+ /**
415
+ * Enable/disable verbose per-frame logging.
416
+ * When true: logs lifecycle events, performance stats, and every frame queue/release/render.
417
+ * When false (default): only warnings and errors.
418
+ */
419
+ public void setVerbose(boolean verbose) {
420
+ this.verbose = verbose;
421
+ // Propagate to active dual-stream player if it exists
422
+ if (bufferedPlayer != null) {
423
+ bufferedPlayer.setVerbose(verbose);
424
+ }
425
+ if (playerId != 0 && !isReleased) {
426
+ WebMAlphaPlayerNative.nativeSetVerbose(playerId, verbose);
427
+ }
428
+ }
429
+
430
+ public void setPaused(boolean paused) {
431
+ this.paused = paused;
432
+ if (paused) {
433
+ pause();
434
+ } else if (autoPlay) {
435
+ play();
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Names what the store keeps a URL's bytes under, instead of the URL (for presigned URLs that
441
+ * change); set it before or together with the source.
442
+ */
443
+ public void setCacheKey(String cacheKey) {
444
+ this.cacheKey = cacheKey;
445
+ }
446
+
447
+ /** Sent with every request for an http(s) source, and given to a data source; null for none. */
448
+ public void setHeaders(Map<String, String> headers) {
449
+ this.headers = headers == null || headers.isEmpty()
450
+ ? Collections.emptyMap() : Collections.unmodifiableMap(new LinkedHashMap<>(headers));
451
+ }
452
+
453
+ /**
454
+ * Reads the source through the data source registered under this name with the range-source
455
+ * add-on ({@code dev.mediacache.rangesource.RangeDataSources.register}), whatever its scheme;
456
+ * null for the built-in ones.
457
+ */
458
+ public void setDataSource(String name) {
459
+ this.dataSource = name == null || name.isEmpty() ? null : name;
460
+ }
461
+
462
+ /**
463
+ * Where a URL's or data source's bytes are kept between plays: {@code "disk"} (in the cache
464
+ * folder), {@code "memory"} (for the process's lifetime) or {@code "none"}. {@code "auto"}, the
465
+ * default, is disk for URLs and none for a data source, whose caching is its own. The store is
466
+ * partial: it keeps what has been played, and a clip held whole plays again offline.
467
+ */
468
+ public void setStorage(String storage) {
469
+ switch (storage == null ? STORAGE_AUTO : storage) {
470
+ case STORAGE_DISK:
471
+ case STORAGE_MEMORY:
472
+ case STORAGE_NONE:
473
+ this.storage = storage;
474
+ break;
475
+ default:
476
+ this.storage = STORAGE_AUTO;
477
+ }
478
+ }
479
+
480
+ /** The cap of this player's store, over every clip in it, least recently played out first. */
481
+ public void setMaxStorageBytes(long maxStorageBytes) {
482
+ this.maxStorageBytes = maxStorageBytes > 0 ? maxStorageBytes : DEFAULT_MAX_STORAGE_BYTES;
483
+ }
484
+
485
+ /**
486
+ * Sets the video: {@code https://} (read by range as it plays), {@code file://}, an absolute
487
+ * path, {@code asset://} (read from the APK in place), {@code content://}, or with
488
+ * {@link #setDataSource} any URI that data source reads.
489
+ */
490
+ public void setSource(String uri) {
491
+ final int generation = ++sourceGeneration;
492
+ sourceUri = uri;
493
+ localSource = null;
494
+ closeReader();
495
+ // Props arriving in the same update (cacheKey, mode, ...) are applied after this one.
496
+ post(() -> {
497
+ if (generation == sourceGeneration) resolveSource(uri, generation);
498
+ });
499
+ }
500
+
501
+ private static boolean isRemote(String uri) {
502
+ return uri.startsWith("https://") || uri.startsWith("http://");
503
+ }
504
+
505
+ private void resolveSource(String uri, int generation) {
506
+ final boolean appSource = dataSource != null;
507
+ if (!appSource && !isRemote(uri)) {
508
+ localSource = uri;
509
+ loadLocalSource();
510
+ return;
511
+ }
512
+
513
+ final boolean addOn = RangeSourceLink.available();
514
+ if (!addOn) {
515
+ if (appSource) {
516
+ reportError("The data source \"" + dataSource + "\" needs the range-source add-on "
517
+ + "(dev.mediacache:range-source; for React Native, react-native-range-source), which the app "
518
+ + "doesn't include", ERROR_NO_SOURCE_MODULE);
519
+ return;
520
+ }
521
+ warnWithoutAddOn();
522
+ }
523
+
524
+ final Fetcher source;
525
+ if (appSource) {
526
+ source = AppDataSource.create(dataSource);
527
+ if (source == null) {
528
+ reportError("No data source is registered as \"" + dataSource + "\" (RangeDataSources.register)");
529
+ return;
530
+ }
531
+ } else {
532
+ source = new HttpFetcher();
533
+ }
534
+ final SourceReader.Storage readerStorage = storageFor(appSource);
535
+ final String storeDir = readerStorage == SourceReader.Storage.DISK
536
+ ? new File(getContext().getCacheDir(), STORE_NAME).getPath() : STORE_NAME;
537
+ final String key = cacheKey != null && !cacheKey.isEmpty() ? cacheKey : uri;
538
+ final Map<String, String> requestHeaders = headers;
539
+ final long maxBytes = maxStorageBytes;
540
+ final String downloadDir = getContext().getCacheDir().getPath();
541
+ // The stream modes start on the first bytes. Preload decodes the whole clip before
542
+ // showing anything, and would hold the global decode queue for the whole transfer.
543
+ final boolean fetchFirst = MODE_PRELOAD.equals(mode);
544
+ probeExecutor.execute(() -> {
545
+ SourceReader opened = null;
546
+ String error = null;
547
+ try {
548
+ opened = SourceReader.open(source, Uri.parse(uri), requestHeaders, readerStorage, storeDir, key, maxBytes,
549
+ downloadDir);
550
+ if (fetchFirst) opened.fetchAll();
551
+ } catch (IOException e) {
552
+ error = e.getMessage();
553
+ if (opened != null) opened.close();
554
+ opened = null;
555
+ }
556
+ final SourceReader result = opened;
557
+ final String failure = error;
558
+ post(() -> {
559
+ if (generation != sourceGeneration) {
560
+ if (result != null) result.close();
561
+ return;
562
+ }
563
+ if (result == null) {
564
+ reportError("Loading " + uri + " failed: " + failure);
565
+ return;
566
+ }
567
+ reader = result;
568
+ localSource = uri;
569
+ loadLocalSource();
570
+ });
571
+ });
572
+ }
573
+
574
+ private SourceReader.Storage storageFor(boolean appSource) {
575
+ switch (storage) {
576
+ case STORAGE_DISK: return SourceReader.Storage.DISK;
577
+ case STORAGE_MEMORY: return SourceReader.Storage.MEMORY;
578
+ case STORAGE_NONE: return SourceReader.Storage.NONE;
579
+ default: return appSource ? SourceReader.Storage.NONE : SourceReader.Storage.DISK;
580
+ }
581
+ }
582
+
583
+ private void warnWithoutAddOn() {
584
+ if (warnedWithoutAddOn.compareAndSet(false, true)) {
585
+ Log.w(TAG, "range-source isn't installed (dev.mediacache:range-source; for React Native, "
586
+ + "npm i react-native-range-source): URLs are downloaded whole for each play, without range "
587
+ + "requests, storage, maxStorageBytes or cacheKey");
588
+ }
589
+ }
590
+
591
+ /** Drops this view's reference; a player still reading keeps the reader until it unloads. */
592
+ private void closeReader() {
593
+ if (reader != null) {
594
+ reader.close();
595
+ reader = null;
596
+ }
597
+ }
598
+
599
+ /** Loads {@link #localSource} if there is one and the surface is ready; otherwise waits for it. */
600
+ private void loadLocalSource() {
601
+ String uri = localSource;
602
+ if (uri == null) return;
603
+ if (!glReady) {
604
+ logV("Deferring load until the surface is ready: " + uri);
605
+ return;
606
+ }
607
+ if (reader != null) {
608
+ probeReader(uri, reader);
609
+ return;
610
+ }
611
+
612
+ final WebMSource source;
613
+ final int[] colorAlphaTracks;
614
+ try {
615
+ source = WebMSource.parse(uri);
616
+ try (AssetFileDescriptor afd = source.open(getContext())) {
617
+ // Colour and alpha as separate VP8/VP9/AV1 bitstreams (2 tracks, or VP8/VP9 alpha
618
+ // in BlockAdditional) decode on MediaCodec; anything else on the software decoder
619
+ colorAlphaTracks = BufferedDualStreamPlayer.nativeColorAlphaTracks(
620
+ afd.getParcelFileDescriptor().getFd(), afd.getStartOffset(), afd.getLength());
621
+ }
622
+ } catch (IllegalArgumentException | IOException e) {
623
+ reportError(e.getMessage());
624
+ return;
625
+ }
626
+ loadProbedSource(source, colorAlphaTracks);
627
+ }
628
+
629
+ private void probeReader(String uri, SourceReader sourceReader) {
630
+ final int generation = sourceGeneration;
631
+ final long reference = sourceReader.retain();
632
+ if (reference == 0) return;
633
+ probeExecutor.execute(() -> {
634
+ int[] colorAlphaTracks;
635
+ try {
636
+ colorAlphaTracks = BufferedDualStreamPlayer.nativeColorAlphaTracksReader(reference);
637
+ } finally {
638
+ SourceReader.release(reference);
639
+ }
640
+ post(() -> {
641
+ if (generation != sourceGeneration || sourceReader != reader || isReleased || !glReady) return;
642
+ loadProbedSource(WebMSource.reader(uri, sourceReader), colorAlphaTracks);
643
+ });
644
+ });
645
+ }
646
+
647
+ /** @param colorAlphaTracks from {@link BufferedDualStreamPlayer#nativeColorAlphaTracks}; null if none */
648
+ private void loadProbedSource(WebMSource source, int[] colorAlphaTracks) {
649
+ logV("Source " + source + " isDualStream=" + (colorAlphaTracks != null) + " decoder=" + decoder);
650
+ boolean forcedMediaCodec = decoder.startsWith(DECODER_MEDIACODEC);
651
+ softwareFallbackReason = null;
652
+
653
+ if (colorAlphaTracks == null) {
654
+ if (forcedMediaCodec) {
655
+ reportError("decoder \"" + decoder + "\" needs colour and alpha as separate VP8, VP9 or AV1 "
656
+ + "bitstreams, and " + source + " has none");
657
+ } else {
658
+ loadSingleStream(source);
659
+ }
660
+ return;
661
+ }
662
+ if (DECODER_SOFTWARE.equals(decoder)) {
663
+ loadSingleStream(source);
664
+ return;
665
+ }
666
+
667
+ // This view's previous player gives back its decoders before the new file reserves some
668
+ if (bufferedPlayer != null) {
669
+ bufferedPlayer.unload();
670
+ bufferedPlayer = null;
671
+ }
672
+ String[] names;
673
+ switch (decoder) {
674
+ case DECODER_MEDIACODEC_HW:
675
+ names = MediaCodecDecoders.reserve(colorAlphaTracks, MediaCodecDecoders.Kind.HARDWARE);
676
+ break;
677
+ case DECODER_MEDIACODEC_SW:
678
+ names = MediaCodecDecoders.reserve(colorAlphaTracks, MediaCodecDecoders.Kind.SOFTWARE);
679
+ break;
680
+ case DECODER_MEDIACODEC:
681
+ names = MediaCodecDecoders.reserve(colorAlphaTracks, MediaCodecDecoders.Kind.ANY);
682
+ break;
683
+ default:
684
+ names = MediaCodecDecoders.reserve(colorAlphaTracks, MediaCodecDecoders.Kind.HARDWARE);
685
+ if (names == null) {
686
+ names = MediaCodecDecoders.reserve(colorAlphaTracks, MediaCodecDecoders.Kind.SOFTWARE);
687
+ }
688
+ if (names == null) {
689
+ logV("No MediaCodec decoders free that list the profiles; decoding in software");
690
+ softwareFallbackReason = "No MediaCodec decoders free that list the profiles of " + source;
691
+ loadSingleStream(source);
692
+ return;
693
+ }
694
+ }
695
+ if (names == null) {
696
+ reportError("No MediaCodec decoders free that list the profiles of " + source
697
+ + " (decoder \"" + decoder + "\")");
698
+ return;
699
+ }
700
+ loadDualStream(source, names);
701
+ }
702
+
703
+ /**
704
+ * Load a dual-stream WebM using MediaCodec decoding.
705
+ * Uses direct OES texture compositing — no FBO ring buffer.
706
+ *
707
+ * @param decoderNames reserved by {@link MediaCodecDecoders#reserve} for this load, which the
708
+ * new player then holds
709
+ */
710
+ private void loadDualStream(WebMSource source, String[] decoderNames) {
711
+ logV("Loading dual-stream source with direct OES pipeline: " + source);
712
+
713
+ isDualStreamMode = true;
714
+
715
+ // Release any existing single-stream player
716
+ if (playerId != 0) {
717
+ WebMAlphaPlayerNative.nativeDestroy(playerId);
718
+ playerId = 0;
719
+ }
720
+
721
+ // Release any existing player
722
+ if (bufferedPlayer != null) {
723
+ bufferedPlayer.unload();
724
+ bufferedPlayer = null;
725
+ }
726
+
727
+ // Create player — must be done on GL thread for OES texture creation
728
+ renderThread.execute(() -> {
729
+ if (isReleased) {
730
+ MediaCodecDecoders.release(decoderNames);
731
+ return;
732
+ }
733
+
734
+ bufferedPlayer = new BufferedDualStreamPlayer();
735
+ bufferedPlayer.useDecoders(decoderNames);
736
+ bufferedPlayer.setLoop(loop);
737
+ bufferedPlayer.setVerbose(verbose);
738
+ // A rate set as a prop arrived long before this player existed; apply it here or
739
+ // the first render would run at 1x regardless of what was asked for.
740
+ bufferedPlayer.setPlaybackSpeed(playbackSpeed);
741
+
742
+ // GL executor posts tasks to the render thread
743
+ bufferedPlayer.setGLExecutor(task -> {
744
+ if (renderThread != null && !renderThread.isShutdown()) {
745
+ renderThread.execute(task);
746
+ }
747
+ });
748
+
749
+ bufferedPlayer.setEventCallback(new BufferedDualStreamPlayer.EventCallback() {
750
+ @Override
751
+ public void onLoaded(int totalFrames, int width, int height, double fps, double duration) {
752
+ logV("Dual-stream loaded (OES): " + totalFrames + " frames, " +
753
+ width + "x" + height + " @ " + String.format("%.1f", fps) + " fps");
754
+ post(() -> {
755
+ if (callback != null && !isReleased) {
756
+ callback.onLoad(totalFrames, width, height, MediaCodecDecoders.describe(decoderNames));
757
+ }
758
+ if (autoPlay && !paused && bufferedPlayer != null) {
759
+ bufferedPlayer.play();
760
+ }
761
+ });
762
+ }
763
+
764
+ @Override
765
+ public void onError(String error) {
766
+ post(() -> reportError(error));
767
+ }
768
+
769
+ @Override
770
+ public void onLooped() {
771
+ logV("Dual-stream player looped");
772
+ }
773
+
774
+ @Override
775
+ public void onEnded() {
776
+ post(() -> {
777
+ if (callback != null && !isReleased) {
778
+ callback.onEnd();
779
+ }
780
+ });
781
+ }
782
+ });
783
+
784
+ // Render callback — playback loop signals when it's time to present a frame
785
+ bufferedPlayer.setRenderCallback((frameIndex) -> {
786
+ if (isReleased) return;
787
+ requestRender();
788
+ });
789
+
790
+ // Load the file (creates OES textures in this GL context)
791
+ boolean loaded;
792
+ if (source.reader() != null) {
793
+ loaded = bufferedPlayer.loadFromReader(source.reader());
794
+ } else {
795
+ try (AssetFileDescriptor afd = source.open(getContext())) {
796
+ loaded = bufferedPlayer.loadFromFd(
797
+ afd.getParcelFileDescriptor().getFd(), afd.getStartOffset(), afd.getLength());
798
+ } catch (IOException e) {
799
+ loaded = false;
800
+ }
801
+ }
802
+ if (!loaded) {
803
+ post(() -> reportError("Failed to load dual-stream video " + source));
804
+ }
805
+ });
806
+ }
807
+
808
+ /**
809
+ * Load a single-stream WebM (VP9 with alpha) using software decoding.
810
+ */
811
+ private void loadSingleStream(WebMSource source) {
812
+ logV("Loading single-stream source with software decoder: " + source);
813
+
814
+ isDualStreamMode = false;
815
+
816
+ // Release any existing dual-stream player
817
+ if (bufferedPlayer != null) {
818
+ bufferedPlayer.unload();
819
+ bufferedPlayer = null;
820
+ }
821
+
822
+ // Initialize software player if needed - by now all React props should be set
823
+ if (playerId == 0 && !isReleased) {
824
+ initializePlayer();
825
+ }
826
+
827
+ if (isReleased || playerId == 0) {
828
+ Log.w(TAG, "Cannot load - player not initialized");
829
+ return;
830
+ }
831
+
832
+ final Context context = getContext();
833
+ enqueueNativeLoad(() -> {
834
+ SourceReader sourceReader = source.reader();
835
+ if (sourceReader != null) {
836
+ long reference = sourceReader.retain();
837
+ if (reference == 0) return false;
838
+ try {
839
+ return WebMAlphaPlayerNative.nativeLoadFromReader(playerId, reference);
840
+ } finally {
841
+ SourceReader.release(reference);
842
+ }
843
+ }
844
+ try (AssetFileDescriptor afd = source.open(context)) {
845
+ return WebMAlphaPlayerNative.nativeLoadFromFd(playerId,
846
+ afd.getParcelFileDescriptor().getFd(), afd.getStartOffset(), afd.getLength());
847
+ }
848
+ });
849
+ }
850
+
851
+ public void loadFromBytes(byte[] data) {
852
+ ++sourceGeneration;
853
+ sourceUri = null;
854
+ localSource = null;
855
+ closeReader();
856
+
857
+ if (isReleased || playerId == 0) {
858
+ Log.w(TAG, "Cannot load from bytes before player is initialized");
859
+ return;
860
+ }
861
+
862
+ enqueueNativeLoad(() -> WebMAlphaPlayerNative.nativeLoadFromBytes(playerId, data));
863
+ }
864
+
865
+ public void play() {
866
+ if (isReleased) return;
867
+
868
+ if (isDualStreamMode && bufferedPlayer != null) {
869
+ bufferedPlayer.play();
870
+ } else if (playerId != 0) {
871
+ WebMAlphaPlayerNative.nativePlay(playerId);
872
+ }
873
+ }
874
+
875
+ public void pause() {
876
+ if (isReleased) return;
877
+
878
+ if (isDualStreamMode && bufferedPlayer != null) {
879
+ bufferedPlayer.pause();
880
+ } else if (playerId != 0) {
881
+ WebMAlphaPlayerNative.nativePause(playerId);
882
+ }
883
+ }
884
+
885
+ public void stop() {
886
+ if (isReleased) return;
887
+
888
+ if (isDualStreamMode && bufferedPlayer != null) {
889
+ bufferedPlayer.stop();
890
+ } else if (playerId != 0) {
891
+ WebMAlphaPlayerNative.nativeStop(playerId);
892
+ }
893
+ }
894
+
895
+ /**
896
+ * Moves to a frame (clamped to the clip), staying playing or paused; a paused view shows it.
897
+ * Ignored until the clip has loaded.
898
+ */
899
+ public void seekToFrame(int frame) {
900
+ if (isReleased) return;
901
+
902
+ if (isDualStreamMode && bufferedPlayer != null) {
903
+ bufferedPlayer.seekToFrame(frame);
904
+ } else if (playerId != 0) {
905
+ WebMAlphaPlayerNative.nativeSeekToFrame(playerId, frame);
906
+ }
907
+ }
908
+
909
+ /** Moves to the frame showing at {@code seconds}; see {@link #seekToFrame}. */
910
+ public void seekToTime(double seconds) {
911
+ if (isReleased) return;
912
+
913
+ if (isDualStreamMode && bufferedPlayer != null) {
914
+ bufferedPlayer.seekToTime(seconds);
915
+ } else if (playerId != 0) {
916
+ WebMAlphaPlayerNative.nativeSeekToTime(playerId, seconds);
917
+ }
918
+ }
919
+
920
+ public boolean isLoaded() {
921
+ if (isReleased) return false;
922
+
923
+ if (isDualStreamMode && bufferedPlayer != null) {
924
+ return bufferedPlayer.isLoaded();
925
+ } else if (playerId != 0) {
926
+ return WebMAlphaPlayerNative.nativeIsLoaded(playerId);
927
+ }
928
+ return false;
929
+ }
930
+
931
+ public boolean isPlaying() {
932
+ if (isReleased) return false;
933
+
934
+ if (isDualStreamMode && bufferedPlayer != null) {
935
+ return bufferedPlayer.isPlaying();
936
+ } else if (playerId != 0) {
937
+ return WebMAlphaPlayerNative.nativeIsPlaying(playerId);
938
+ }
939
+ return false;
940
+ }
941
+
942
+ public int getTotalFrames() {
943
+ if (isReleased) return 0;
944
+
945
+ if (isDualStreamMode && bufferedPlayer != null) {
946
+ return bufferedPlayer.getTotalFrames();
947
+ } else if (playerId != 0) {
948
+ return WebMAlphaPlayerNative.nativeGetTotalFrames(playerId);
949
+ }
950
+ return 0;
951
+ }
952
+
953
+ public int getVideoWidth() {
954
+ if (isReleased) return 0;
955
+
956
+ if (isDualStreamMode && bufferedPlayer != null) {
957
+ return bufferedPlayer.getWidth();
958
+ } else if (playerId != 0) {
959
+ return WebMAlphaPlayerNative.nativeGetWidth(playerId);
960
+ }
961
+ return 0;
962
+ }
963
+
964
+ public int getVideoHeight() {
965
+ if (isReleased) return 0;
966
+
967
+ if (isDualStreamMode && bufferedPlayer != null) {
968
+ return bufferedPlayer.getHeight();
969
+ } else if (playerId != 0) {
970
+ return WebMAlphaPlayerNative.nativeGetHeight(playerId);
971
+ }
972
+ return 0;
973
+ }
974
+
975
+ //------------------------------------------------------------------------------
976
+ // SurfaceTextureListener
977
+ //------------------------------------------------------------------------------
978
+
979
+ @Override
980
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
981
+ logV("Surface available: " + width + "x" + height);
982
+ this.surfaceTexture = surface;
983
+ this.surfaceWidth = width;
984
+ this.surfaceHeight = height;
985
+ surfaceAvailable.set(true);
986
+
987
+ // DON'T initialize player here - wait until we load a source
988
+ // This ensures all React props (including mode) are set before player creation
989
+
990
+ // Create render thread if needed (for EGL init)
991
+ if (renderThread == null || renderThread.isShutdown()) {
992
+ renderThread = Executors.newSingleThreadExecutor(r -> {
993
+ Thread t = new Thread(r, "WebMGLRender");
994
+ t.setPriority(Thread.MAX_PRIORITY);
995
+ return t;
996
+ });
997
+ }
998
+
999
+ // Initialize EGL on render thread, then reload source
1000
+ renderThread.execute(() -> {
1001
+ initEGL();
1002
+
1003
+ // Loading dispatches to renderThread internally, so post to the main thread
1004
+ // to avoid deadlocking the single-threaded executor
1005
+ post(() -> {
1006
+ glReady = true;
1007
+ if (isReleased || localSource == null) return;
1008
+ waitForSoftRelease();
1009
+ loadLocalSource();
1010
+ });
1011
+ });
1012
+ }
1013
+
1014
+ @Override
1015
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
1016
+ logV("Surface size changed: " + width + "x" + height);
1017
+ this.surfaceWidth = width;
1018
+ this.surfaceHeight = height;
1019
+
1020
+ // Update viewport on render thread
1021
+ onRenderThread(() -> {
1022
+ if (eglSurface != null && eglContext != null) {
1023
+ makeCurrent();
1024
+ GLES20.glViewport(0, 0, width, height);
1025
+ }
1026
+ });
1027
+ }
1028
+
1029
+ @Override
1030
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
1031
+ logV("Surface destroyed");
1032
+ surfaceAvailable.set(false);
1033
+ glReady = false;
1034
+
1035
+ // Store reference before nulling so we can release it async
1036
+ final SurfaceTexture surfaceToRelease = this.surfaceTexture;
1037
+ this.surfaceTexture = null;
1038
+
1039
+ // Clean up EGL on render thread WITHOUT blocking main thread
1040
+ // This prevents ANRs when multiple players are destroyed simultaneously.
1041
+ // After release() there is no render thread, and release() has cleaned up already.
1042
+ onRenderThread(this::destroyEGLSafely);
1043
+
1044
+ // Return false - we'll manage SurfaceTexture lifecycle ourselves to avoid blocking
1045
+ // The render thread will clean up EGL and we don't need the SurfaceTexture after that
1046
+ return true;
1047
+ }
1048
+
1049
+ @Override
1050
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
1051
+ // Called when updateTexImage() is called, usually nothing to do here
1052
+ }
1053
+
1054
+ //------------------------------------------------------------------------------
1055
+ // EGL Setup (runs on render thread)
1056
+ //------------------------------------------------------------------------------
1057
+
1058
+ /** Picks an RGBA8 window config of the renderable type and creates a context of that version. */
1059
+ private boolean createContext(int renderableType, int clientVersion) {
1060
+ int[] configAttribs = {
1061
+ EGL14.EGL_RED_SIZE, 8,
1062
+ EGL14.EGL_GREEN_SIZE, 8,
1063
+ EGL14.EGL_BLUE_SIZE, 8,
1064
+ EGL14.EGL_ALPHA_SIZE, 8,
1065
+ EGL14.EGL_RENDERABLE_TYPE, renderableType,
1066
+ EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
1067
+ EGL14.EGL_NONE
1068
+ };
1069
+ EGLConfig[] configs = new EGLConfig[1];
1070
+ int[] numConfigs = new int[1];
1071
+ if (!EGL14.eglChooseConfig(eglDisplay, configAttribs, 0, configs, 0, 1, numConfigs, 0) || numConfigs[0] == 0) {
1072
+ return false;
1073
+ }
1074
+ int[] contextAttribs = { EGL14.EGL_CONTEXT_CLIENT_VERSION, clientVersion, EGL14.EGL_NONE };
1075
+ EGLContext context = EGL14.eglCreateContext(eglDisplay, configs[0], EGL14.EGL_NO_CONTEXT, contextAttribs, 0);
1076
+ if (context == EGL14.EGL_NO_CONTEXT) return false;
1077
+ eglConfig = configs[0];
1078
+ eglContext = context;
1079
+ return true;
1080
+ }
1081
+
1082
+ private void initEGL() {
1083
+ logV("Initializing EGL");
1084
+
1085
+ // Get display
1086
+ eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
1087
+ if (eglDisplay == EGL14.EGL_NO_DISPLAY) {
1088
+ Log.e(TAG, "Unable to get EGL display");
1089
+ return;
1090
+ }
1091
+
1092
+ // Initialize
1093
+ int[] version = new int[2];
1094
+ if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) {
1095
+ Log.e(TAG, "Unable to initialize EGL");
1096
+ return;
1097
+ }
1098
+
1099
+ // ES 3 lets the dual-stream shader read raw YUV (GL_EXT_YUV_target); ES 2 otherwise
1100
+ glesVersion = 3;
1101
+ if (!createContext(EGLExt.EGL_OPENGL_ES3_BIT_KHR, 3)) {
1102
+ glesVersion = 2;
1103
+ if (!createContext(EGL14.EGL_OPENGL_ES2_BIT, 2)) {
1104
+ Log.e(TAG, "Unable to create EGL context");
1105
+ return;
1106
+ }
1107
+ }
1108
+
1109
+ // Create window surface from SurfaceTexture
1110
+ int[] surfaceAttribs = { EGL14.EGL_NONE };
1111
+ eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, eglConfig, surfaceTexture, surfaceAttribs, 0);
1112
+ if (eglSurface == EGL14.EGL_NO_SURFACE) {
1113
+ Log.e(TAG, "Unable to create EGL surface");
1114
+ return;
1115
+ }
1116
+
1117
+ // Make current and initialize GL resources
1118
+ makeCurrent();
1119
+ initGL();
1120
+
1121
+ logV("EGL initialized successfully");
1122
+ }
1123
+
1124
+ private void destroyEGL() {
1125
+ logV("Destroying EGL");
1126
+
1127
+ if (eglDisplay != null) {
1128
+ EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
1129
+
1130
+ if (eglSurface != null) {
1131
+ EGL14.eglDestroySurface(eglDisplay, eglSurface);
1132
+ eglSurface = null;
1133
+ }
1134
+
1135
+ if (eglContext != null) {
1136
+ EGL14.eglDestroyContext(eglDisplay, eglContext);
1137
+ eglContext = null;
1138
+ }
1139
+
1140
+ EGL14.eglTerminate(eglDisplay);
1141
+ eglDisplay = null;
1142
+ }
1143
+
1144
+ // Reset GL state
1145
+ programId = 0;
1146
+ yuvOesProgramId = 0;
1147
+ yTextureId = uTextureId = vTextureId = aTextureId = 0;
1148
+ currentWidth = currentHeight = 0;
1149
+ }
1150
+
1151
+ private void makeCurrent() {
1152
+ if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
1153
+ Log.e(TAG, "eglMakeCurrent failed");
1154
+ }
1155
+ }
1156
+
1157
+ //------------------------------------------------------------------------------
1158
+ // OpenGL Setup (runs on render thread after EGL init)
1159
+ //------------------------------------------------------------------------------
1160
+
1161
+ private void initGL() {
1162
+ GLES20.glClearColor(0f, 0f, 0f, 0f);
1163
+
1164
+ // Create YUVA shader program (for ByteBuffer mode - software decoded)
1165
+ String vertexShader =
1166
+ "attribute vec4 aPosition;\n" +
1167
+ "attribute vec2 aTexCoord;\n" +
1168
+ "varying vec2 vTexCoord;\n" +
1169
+ "void main() {\n" +
1170
+ " gl_Position = aPosition;\n" +
1171
+ " vTexCoord = aTexCoord;\n" +
1172
+ "}\n";
1173
+
1174
+ String fragmentShader =
1175
+ "precision mediump float;\n" +
1176
+ "varying vec2 vTexCoord;\n" +
1177
+ "uniform sampler2D yTexture;\n" +
1178
+ "uniform sampler2D uTexture;\n" +
1179
+ "uniform sampler2D vTexture;\n" +
1180
+ "uniform sampler2D aTexture;\n" +
1181
+ "uniform mat4 uYuvToRgb;\n" +
1182
+ "void main() {\n" +
1183
+ " vec4 yuv = vec4(texture2D(yTexture, vTexCoord).r, texture2D(uTexture, vTexCoord).r,\n" +
1184
+ " texture2D(vTexture, vTexCoord).r, 1.0);\n" +
1185
+ " float a = texture2D(aTexture, vTexCoord).r;\n" +
1186
+ " vec3 rgb = clamp((uYuvToRgb * yuv).rgb, 0.0, 1.0);\n" +
1187
+ " gl_FragColor = vec4(rgb * a, a);\n" +
1188
+ "}\n";
1189
+
1190
+ programId = createProgram(vertexShader, fragmentShader);
1191
+
1192
+ // Create OES shader program (for Surface mode - GPU accelerated, zero-copy)
1193
+ // Uses external OES textures which stay on GPU
1194
+ String oesVertexShader =
1195
+ "attribute vec4 aPosition;\n" +
1196
+ "attribute vec2 aTexCoord;\n" +
1197
+ "uniform mat4 uColorSTMatrix;\n" +
1198
+ "uniform mat4 uAlphaSTMatrix;\n" +
1199
+ "varying vec2 vColorTexCoord;\n" +
1200
+ "varying vec2 vAlphaTexCoord;\n" +
1201
+ "void main() {\n" +
1202
+ " gl_Position = aPosition;\n" +
1203
+ " vColorTexCoord = (uColorSTMatrix * vec4(aTexCoord, 0.0, 1.0)).xy;\n" +
1204
+ " vAlphaTexCoord = (uAlphaSTMatrix * vec4(aTexCoord, 0.0, 1.0)).xy;\n" +
1205
+ "}\n";
1206
+
1207
+ // OES fragment shader - samples from external textures (MediaCodec output)
1208
+ // Color texture is YUV, alpha texture Y channel contains alpha
1209
+ String oesFragmentShader =
1210
+ "#extension GL_OES_EGL_image_external : require\n" +
1211
+ "precision mediump float;\n" +
1212
+ "varying vec2 vColorTexCoord;\n" +
1213
+ "varying vec2 vAlphaTexCoord;\n" +
1214
+ "uniform samplerExternalOES uColorTexture;\n" +
1215
+ "uniform samplerExternalOES uAlphaTexture;\n" +
1216
+ "void main() {\n" +
1217
+ " vec4 color = texture2D(uColorTexture, vColorTexCoord);\n" +
1218
+ " float alpha = texture2D(uAlphaTexture, vAlphaTexCoord).r;\n" +
1219
+ " gl_FragColor = vec4(color.rgb * alpha, alpha);\n" +
1220
+ "}\n";
1221
+
1222
+ oesProgramId = createProgram(oesVertexShader, oesFragmentShader);
1223
+ if (oesProgramId == 0) {
1224
+ Log.w(TAG, "OES shader creation failed - Surface mode will not work");
1225
+ } else {
1226
+ logV("OES shader created (GPU-accelerated compositing)");
1227
+ }
1228
+
1229
+ String extensions = GLES20.glGetString(GLES20.GL_EXTENSIONS);
1230
+ if (glesVersion >= 3 && extensions != null && extensions.contains("GL_EXT_YUV_target")) {
1231
+ String yuvOesVertexShader =
1232
+ "#version 300 es\n" +
1233
+ "in vec4 aPosition;\n" +
1234
+ "in vec2 aTexCoord;\n" +
1235
+ "uniform mat4 uColorSTMatrix;\n" +
1236
+ "uniform mat4 uAlphaSTMatrix;\n" +
1237
+ "out vec2 vColorTexCoord;\n" +
1238
+ "out vec2 vAlphaTexCoord;\n" +
1239
+ "void main() {\n" +
1240
+ " gl_Position = aPosition;\n" +
1241
+ " vColorTexCoord = (uColorSTMatrix * vec4(aTexCoord, 0.0, 1.0)).xy;\n" +
1242
+ " vAlphaTexCoord = (uAlphaSTMatrix * vec4(aTexCoord, 0.0, 1.0)).xy;\n" +
1243
+ "}\n";
1244
+ String yuvOesFragmentShader =
1245
+ "#version 300 es\n" +
1246
+ "#extension GL_EXT_YUV_target : require\n" +
1247
+ "precision highp float;\n" +
1248
+ "in vec2 vColorTexCoord;\n" +
1249
+ "in vec2 vAlphaTexCoord;\n" +
1250
+ "uniform __samplerExternal2DY2YEXT uColorTexture;\n" +
1251
+ "uniform __samplerExternal2DY2YEXT uAlphaTexture;\n" +
1252
+ "uniform mat4 uYuvToRgb;\n" +
1253
+ "uniform vec2 uAlphaRange;\n" +
1254
+ "out vec4 fragColor;\n" +
1255
+ "void main() {\n" +
1256
+ " vec3 yuv = texture(uColorTexture, vColorTexCoord).xyz;\n" +
1257
+ " vec3 rgb = clamp((uYuvToRgb * vec4(yuv, 1.0)).rgb, 0.0, 1.0);\n" +
1258
+ " float y = texture(uAlphaTexture, vAlphaTexCoord).x;\n" +
1259
+ " float alpha = clamp(uAlphaRange.x * (y - uAlphaRange.y), 0.0, 1.0);\n" +
1260
+ " fragColor = vec4(rgb * alpha, alpha);\n" +
1261
+ "}\n";
1262
+ yuvOesProgramId = createLinkedProgram(yuvOesVertexShader, yuvOesFragmentShader);
1263
+ logV("Raw YUV OES shader " + (yuvOesProgramId != 0 ? "created" : "failed; the driver converts"));
1264
+ }
1265
+
1266
+ // Create textures for YUVA mode
1267
+ int[] textures = new int[4];
1268
+ GLES20.glGenTextures(4, textures, 0);
1269
+ yTextureId = textures[0];
1270
+ uTextureId = textures[1];
1271
+ vTextureId = textures[2];
1272
+ aTextureId = textures[3];
1273
+
1274
+ // Set up vertex buffer (fullscreen quad with flipped Y for TextureView/YUVA mode)
1275
+ float[] vertices = {
1276
+ -1f, -1f, 0f, 1f, // bottom-left
1277
+ 1f, -1f, 1f, 1f, // bottom-right
1278
+ -1f, 1f, 0f, 0f, // top-left
1279
+ 1f, 1f, 1f, 0f // top-right
1280
+ };
1281
+ vertexBuffer = ByteBuffer.allocateDirect(vertices.length * 4)
1282
+ .order(ByteOrder.nativeOrder())
1283
+ .asFloatBuffer()
1284
+ .put(vertices);
1285
+ vertexBuffer.position(0);
1286
+
1287
+ // Set up OES vertex buffer with UN-FLIPPED texture coordinates
1288
+ // SurfaceTexture's transformation matrix will handle the flip
1289
+ float[] oesVertices = {
1290
+ -1f, -1f, 0f, 0f, // bottom-left (tex coord: 0,0)
1291
+ 1f, -1f, 1f, 0f, // bottom-right (tex coord: 1,0)
1292
+ -1f, 1f, 0f, 1f, // top-left (tex coord: 0,1)
1293
+ 1f, 1f, 1f, 1f // top-right (tex coord: 1,1)
1294
+ };
1295
+ oesVertexBuffer = ByteBuffer.allocateDirect(oesVertices.length * 4)
1296
+ .order(ByteOrder.nativeOrder())
1297
+ .asFloatBuffer()
1298
+ .put(oesVertices);
1299
+ oesVertexBuffer.position(0);
1300
+
1301
+ // Initialize identity matrices for OES
1302
+ android.opengl.Matrix.setIdentityM(colorTransformMatrix, 0);
1303
+ android.opengl.Matrix.setIdentityM(alphaTransformMatrix, 0);
1304
+
1305
+ // Enable blending for transparency
1306
+ GLES20.glEnable(GLES20.GL_BLEND);
1307
+ GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
1308
+
1309
+ // Set viewport
1310
+ GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight);
1311
+
1312
+ logV("OpenGL initialized");
1313
+ }
1314
+
1315
+ private int createProgram(String vertexSource, String fragmentSource) {
1316
+ int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
1317
+ int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
1318
+
1319
+ int program = GLES20.glCreateProgram();
1320
+ GLES20.glAttachShader(program, vertexShader);
1321
+ GLES20.glAttachShader(program, fragmentShader);
1322
+ GLES20.glLinkProgram(program);
1323
+
1324
+ return program;
1325
+ }
1326
+
1327
+ /** createProgram, but 0 (logged) when a shader doesn't compile or the program doesn't link. */
1328
+ private int createLinkedProgram(String vertexSource, String fragmentSource) {
1329
+ int program = createProgram(vertexSource, fragmentSource);
1330
+ int[] status = new int[1];
1331
+ GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, status, 0);
1332
+ if (status[0] != 0) return program;
1333
+ Log.w(TAG, "Shader program failed to link: " + GLES20.glGetProgramInfoLog(program));
1334
+ GLES20.glDeleteProgram(program);
1335
+ return 0;
1336
+ }
1337
+
1338
+ private int loadShader(int type, String source) {
1339
+ int shader = GLES20.glCreateShader(type);
1340
+ GLES20.glShaderSource(shader, source);
1341
+ GLES20.glCompileShader(shader);
1342
+ int[] status = new int[1];
1343
+ GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0);
1344
+ if (status[0] == 0) Log.w(TAG, "Shader failed to compile: " + GLES20.glGetShaderInfoLog(shader));
1345
+ return shader;
1346
+ }
1347
+
1348
+ //------------------------------------------------------------------------------
1349
+ // Rendering
1350
+ //------------------------------------------------------------------------------
1351
+
1352
+ private void requestRender() {
1353
+ // Prevent queuing multiple render calls
1354
+ if (renderPending.compareAndSet(false, true) && !onRenderThread(this::doRender)) {
1355
+ renderPending.set(false);
1356
+ }
1357
+ }
1358
+
1359
+ /** Runs a task on the render thread. False when there is none, after {@link #release()}. */
1360
+ private boolean onRenderThread(Runnable task) {
1361
+ ExecutorService thread = renderThread;
1362
+ if (thread == null || thread.isShutdown()) return false;
1363
+ try {
1364
+ thread.execute(task);
1365
+ return true;
1366
+ } catch (RejectedExecutionException e) {
1367
+ return false;
1368
+ }
1369
+ }
1370
+
1371
+ private void destroyEGLSafely() {
1372
+ try {
1373
+ destroyEGL();
1374
+ } catch (Exception e) {
1375
+ Log.e(TAG, "Error during EGL cleanup: " + e.getMessage());
1376
+ }
1377
+ }
1378
+
1379
+ private void doRender() {
1380
+ renderPending.set(false);
1381
+
1382
+ if (!surfaceAvailable.get() || eglSurface == null || eglContext == null || isReleased) {
1383
+ return;
1384
+ }
1385
+
1386
+ try {
1387
+ makeCurrent();
1388
+
1389
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
1390
+
1391
+ // Dual-stream mode — composite directly from OES textures
1392
+ if (isDualStreamMode && bufferedPlayer != null) {
1393
+ doRenderDirectOes();
1394
+ return;
1395
+ }
1396
+
1397
+ // ByteBuffer mode (YUVA textures) - for single-stream files
1398
+ doRenderYuva();
1399
+
1400
+ } catch (Exception e) {
1401
+ Log.w(TAG, "Render exception: " + e.getMessage());
1402
+ }
1403
+ }
1404
+
1405
+ /**
1406
+ * Render by compositing directly from OES textures (SurfaceTexture output).
1407
+ * Single GL draw call: fragment shader samples both color and alpha OES textures,
1408
+ * multiplies color by alpha, and outputs premultiplied RGBA.
1409
+ * No intermediate FBO — the SurfaceTexture IS the buffer.
1410
+ */
1411
+ private void doRenderDirectOes() {
1412
+ if (bufferedPlayer == null || oesProgramId == 0) {
1413
+ EGL14.eglSwapBuffers(eglDisplay, eglSurface);
1414
+ return;
1415
+ }
1416
+
1417
+ // Consume frames only when BOTH decoders have released a new frame.
1418
+ // This ensures color and alpha are always from the same temporal position.
1419
+ // If consumeFrames() returns false, we re-render the previous frame (OES
1420
+ // textures retain their content), which is visually smooth.
1421
+ bufferedPlayer.consumeFrames();
1422
+
1423
+ int colorTexId = bufferedPlayer.getColorOesTextureId();
1424
+ int alphaTexId = bufferedPlayer.getAlphaOesTextureId();
1425
+
1426
+ if (colorTexId <= 0 || alphaTexId <= 0) {
1427
+ EGL14.eglSwapBuffers(eglDisplay, eglSurface);
1428
+ return;
1429
+ }
1430
+
1431
+ float[] colorYuvToRgb = bufferedPlayer.getColorYuvToRgb();
1432
+ float[] alphaRange = bufferedPlayer.getAlphaRange();
1433
+ boolean rawYuv = yuvOesProgramId != 0 && colorYuvToRgb != null && alphaRange != null;
1434
+ int program = rawYuv ? yuvOesProgramId : oesProgramId;
1435
+ GLES20.glUseProgram(program);
1436
+
1437
+ // Get attribute/uniform locations
1438
+ int positionHandle = GLES20.glGetAttribLocation(program, "aPosition");
1439
+ int texCoordHandle = GLES20.glGetAttribLocation(program, "aTexCoord");
1440
+ int colorSTMatrixHandle = GLES20.glGetUniformLocation(program, "uColorSTMatrix");
1441
+ int alphaSTMatrixHandle = GLES20.glGetUniformLocation(program, "uAlphaSTMatrix");
1442
+ int colorTextureHandle = GLES20.glGetUniformLocation(program, "uColorTexture");
1443
+ int alphaTextureHandle = GLES20.glGetUniformLocation(program, "uAlphaTexture");
1444
+ if (rawYuv) {
1445
+ GLES20.glUniformMatrix4fv(GLES20.glGetUniformLocation(program, "uYuvToRgb"), 1, false, colorYuvToRgb, 0);
1446
+ GLES20.glUniform2fv(GLES20.glGetUniformLocation(program, "uAlphaRange"), 1, alphaRange, 0);
1447
+ }
1448
+
1449
+ // Set vertex attributes (un-flipped — SurfaceTexture transform handles orientation)
1450
+ oesVertexBuffer.position(0);
1451
+ GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 16, oesVertexBuffer);
1452
+ GLES20.glEnableVertexAttribArray(positionHandle);
1453
+
1454
+ oesVertexBuffer.position(2);
1455
+ GLES20.glVertexAttribPointer(texCoordHandle, 2, GLES20.GL_FLOAT, false, 16, oesVertexBuffer);
1456
+ GLES20.glEnableVertexAttribArray(texCoordHandle);
1457
+
1458
+ // Set SurfaceTexture transform matrices
1459
+ GLES20.glUniformMatrix4fv(colorSTMatrixHandle, 1, false, bufferedPlayer.getColorTransformMatrix(), 0);
1460
+ GLES20.glUniformMatrix4fv(alphaSTMatrixHandle, 1, false, bufferedPlayer.getAlphaTransformMatrix(), 0);
1461
+
1462
+ // Bind OES textures
1463
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
1464
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, colorTexId);
1465
+ GLES20.glUniform1i(colorTextureHandle, 0);
1466
+
1467
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
1468
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, alphaTexId);
1469
+ GLES20.glUniform1i(alphaTextureHandle, 1);
1470
+
1471
+ // Draw fullscreen quad
1472
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
1473
+
1474
+ // Cleanup
1475
+ GLES20.glDisableVertexAttribArray(positionHandle);
1476
+ GLES20.glDisableVertexAttribArray(texCoordHandle);
1477
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
1478
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
1479
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
1480
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
1481
+
1482
+ // Swap buffers
1483
+ EGL14.eglSwapBuffers(eglDisplay, eglSurface);
1484
+ }
1485
+
1486
+ /**
1487
+ * Render using YUVA textures (ByteBuffer mode - slower, CPU upload).
1488
+ * This is the fallback path for single-stream or legacy dual-stream.
1489
+ */
1490
+ private void doRenderYuva() {
1491
+ // Grab frame data under lock
1492
+ ByteBuffer y, u, v, a;
1493
+ int width, height;
1494
+ synchronized (frameDataLock) {
1495
+ // Two requests can arrive for one frame (a seek as the preload finishes); swapping
1496
+ // the cleared buffer for the second would blank a paused view.
1497
+ if (isReleased || !hasNewFrame || pendingY == null) {
1498
+ return;
1499
+ }
1500
+
1501
+ y = pendingY;
1502
+ u = pendingU;
1503
+ v = pendingV;
1504
+ a = pendingA;
1505
+ width = pendingWidth;
1506
+ height = pendingHeight;
1507
+ hasNewFrame = false;
1508
+ }
1509
+
1510
+ if (y == null || u == null || v == null || a == null || width <= 0 || height <= 0) {
1511
+ return;
1512
+ }
1513
+
1514
+ // Update textures
1515
+ GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
1516
+
1517
+ // Y texture
1518
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
1519
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yTextureId);
1520
+ if (currentWidth != width || currentHeight != height) {
1521
+ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
1522
+ width, height, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, y);
1523
+ setupTexture();
1524
+ } else {
1525
+ GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, width, height,
1526
+ GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, y);
1527
+ }
1528
+
1529
+ // U texture (4:2:0, rounded up for odd sizes)
1530
+ int uvWidth = (width + 1) / 2;
1531
+ int uvHeight = (height + 1) / 2;
1532
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
1533
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, uTextureId);
1534
+ if (currentWidth != width || currentHeight != height) {
1535
+ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
1536
+ uvWidth, uvHeight, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, u);
1537
+ setupTexture();
1538
+ } else {
1539
+ GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, uvWidth, uvHeight,
1540
+ GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, u);
1541
+ }
1542
+
1543
+ // V texture
1544
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE2);
1545
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, vTextureId);
1546
+ if (currentWidth != width || currentHeight != height) {
1547
+ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
1548
+ uvWidth, uvHeight, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, v);
1549
+ setupTexture();
1550
+ } else {
1551
+ GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, uvWidth, uvHeight,
1552
+ GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, v);
1553
+ }
1554
+
1555
+ // A texture
1556
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE3);
1557
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, aTextureId);
1558
+ if (currentWidth != width || currentHeight != height) {
1559
+ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
1560
+ width, height, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, a);
1561
+ setupTexture();
1562
+ } else {
1563
+ GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, width, height,
1564
+ GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, a);
1565
+ }
1566
+
1567
+ currentWidth = width;
1568
+ currentHeight = height;
1569
+
1570
+ // Draw
1571
+ GLES20.glUseProgram(programId);
1572
+
1573
+ int positionHandle = GLES20.glGetAttribLocation(programId, "aPosition");
1574
+ int texCoordHandle = GLES20.glGetAttribLocation(programId, "aTexCoord");
1575
+
1576
+ vertexBuffer.position(0);
1577
+ GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 16, vertexBuffer);
1578
+ GLES20.glEnableVertexAttribArray(positionHandle);
1579
+
1580
+ vertexBuffer.position(2);
1581
+ GLES20.glVertexAttribPointer(texCoordHandle, 2, GLES20.GL_FLOAT, false, 16, vertexBuffer);
1582
+ GLES20.glEnableVertexAttribArray(texCoordHandle);
1583
+
1584
+ GLES20.glUniform1i(GLES20.glGetUniformLocation(programId, "yTexture"), 0);
1585
+ GLES20.glUniform1i(GLES20.glGetUniformLocation(programId, "uTexture"), 1);
1586
+ GLES20.glUniform1i(GLES20.glGetUniformLocation(programId, "vTexture"), 2);
1587
+ GLES20.glUniform1i(GLES20.glGetUniformLocation(programId, "aTexture"), 3);
1588
+ GLES20.glUniformMatrix4fv(GLES20.glGetUniformLocation(programId, "uYuvToRgb"), 1, false, yuvToRgb, 0);
1589
+
1590
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
1591
+
1592
+ // Swap buffers
1593
+ EGL14.eglSwapBuffers(eglDisplay, eglSurface);
1594
+ }
1595
+
1596
+ private void setupTexture() {
1597
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
1598
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
1599
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
1600
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
1601
+ }
1602
+
1603
+ //------------------------------------------------------------------------------
1604
+ // PlayerListener callbacks (called from C++ thread)
1605
+ //------------------------------------------------------------------------------
1606
+
1607
+ @Override
1608
+ public void onLoad(int totalFrames, int width, int height) {
1609
+ if (isReleased) return;
1610
+
1611
+ logV("Loaded: " + totalFrames + " frames, " + width + "x" + height);
1612
+ float[] clipYuvToRgb = WebMAlphaPlayerNative.nativeGetYuvToRgb(playerId);
1613
+ if (clipYuvToRgb != null) yuvToRgb = clipYuvToRgb;
1614
+ String decoderName = WebMAlphaPlayerNative.nativeGetDecoderName(playerId);
1615
+
1616
+ post(() -> {
1617
+ if (isReleased) return;
1618
+
1619
+ if (callback != null) {
1620
+ callback.onLoad(totalFrames, width, height, "software (" + decoderName + ")");
1621
+ }
1622
+
1623
+ // Render first frame
1624
+ if (playerId != 0 && !isReleased) {
1625
+ WebMFrame frame = WebMAlphaPlayerNative.nativeGetFrame(playerId, 0);
1626
+ if (frame != null && !isReleased) {
1627
+ synchronized (frameDataLock) {
1628
+ if (isReleased) return;
1629
+
1630
+ reuseY = copyToReusable(frame.getYPlane(), reuseY);
1631
+ reuseU = copyToReusable(frame.getUPlane(), reuseU);
1632
+ reuseV = copyToReusable(frame.getVPlane(), reuseV);
1633
+ reuseA = copyToReusable(frame.getAPlane(), reuseA);
1634
+
1635
+ pendingY = reuseY;
1636
+ pendingU = reuseU;
1637
+ pendingV = reuseV;
1638
+ pendingA = reuseA;
1639
+ pendingWidth = frame.getWidth();
1640
+ pendingHeight = frame.getHeight();
1641
+ hasNewFrame = true;
1642
+ }
1643
+ if (!isReleased) {
1644
+ requestRender();
1645
+ logV("First frame rendered via TextureView pipeline");
1646
+ }
1647
+ }
1648
+ }
1649
+
1650
+ if (autoPlay && !paused && playerId != 0 && !isReleased) {
1651
+ play();
1652
+ }
1653
+ });
1654
+ }
1655
+
1656
+ @Override
1657
+ public void onFrame(int frameIndex, ByteBuffer yPlane, ByteBuffer uPlane,
1658
+ ByteBuffer vPlane, ByteBuffer aPlane, int width, int height) {
1659
+ if (isReleased) return;
1660
+
1661
+ synchronized (frameDataLock) {
1662
+ if (isReleased) return;
1663
+
1664
+ reuseY = copyToReusable(yPlane, reuseY);
1665
+ reuseU = copyToReusable(uPlane, reuseU);
1666
+ reuseV = copyToReusable(vPlane, reuseV);
1667
+ reuseA = copyToReusable(aPlane, reuseA);
1668
+
1669
+ pendingY = reuseY;
1670
+ pendingU = reuseU;
1671
+ pendingV = reuseV;
1672
+ pendingA = reuseA;
1673
+ pendingWidth = width;
1674
+ pendingHeight = height;
1675
+ hasNewFrame = true;
1676
+ }
1677
+
1678
+ if (!isReleased) {
1679
+ requestRender();
1680
+ }
1681
+ }
1682
+
1683
+ private ByteBuffer copyToReusable(ByteBuffer src, ByteBuffer reuse) {
1684
+ if (src == null) return null;
1685
+ src.rewind();
1686
+ int needed = src.remaining();
1687
+
1688
+ if (reuse != null && reuse.capacity() >= needed) {
1689
+ reuse.clear();
1690
+ reuse.put(src);
1691
+ reuse.rewind();
1692
+ return reuse;
1693
+ }
1694
+
1695
+ ByteBuffer dst = ByteBuffer.allocateDirect(needed);
1696
+ dst.put(src);
1697
+ dst.rewind();
1698
+ return dst;
1699
+ }
1700
+
1701
+ @Override
1702
+ public void onPlay() {
1703
+ if (isReleased) return;
1704
+ logV("Playing");
1705
+ }
1706
+
1707
+ @Override
1708
+ public void onPause() {
1709
+ if (isReleased) return;
1710
+ logV("Paused");
1711
+ }
1712
+
1713
+ @Override
1714
+ public void onStop() {
1715
+ if (isReleased) return;
1716
+ logV("Stopped");
1717
+ }
1718
+
1719
+ @Override
1720
+ public void onLoop() {
1721
+ if (isReleased) return;
1722
+ logV("Looped");
1723
+ }
1724
+
1725
+ @Override
1726
+ public void onEnd() {
1727
+ if (isReleased) return;
1728
+ post(() -> {
1729
+ if (callback != null && !isReleased) {
1730
+ callback.onEnd();
1731
+ }
1732
+ });
1733
+ }
1734
+
1735
+ @Override
1736
+ public void onError(String error, String code) {
1737
+ if (isReleased) return;
1738
+ post(() -> {
1739
+ String reason = softwareFallbackReason;
1740
+ reportError(ERROR_NO_DECODER.equals(code) && reason != null ? reason + "; " + error : error, code);
1741
+ });
1742
+ }
1743
+
1744
+ @Override
1745
+ public void onPreloadComplete(int totalFrames, double timeMs) {
1746
+ if (isReleased) return;
1747
+ logV("Preload complete: " + totalFrames + " frames in " + timeMs + "ms");
1748
+ post(() -> {
1749
+ if (callback != null && !isReleased) {
1750
+ callback.onPreloadComplete(totalFrames, timeMs);
1751
+ }
1752
+ });
1753
+ }
1754
+
1755
+ //------------------------------------------------------------------------------
1756
+ // Lifecycle
1757
+ //------------------------------------------------------------------------------
1758
+
1759
+ public void release() {
1760
+ logV("Full release of player " + playerId + " (dualStream=" + isDualStreamMode + ")");
1761
+
1762
+ final long pid = playerId;
1763
+ playerId = 0;
1764
+ isReleased = true;
1765
+ // A reader still opening is closed when it arrives
1766
+ ++sourceGeneration;
1767
+ closeReader();
1768
+
1769
+ cancelNativeLoad();
1770
+
1771
+ // Release buffered dual-stream player if in use
1772
+ if (bufferedPlayer != null) {
1773
+ bufferedPlayer.unload();
1774
+ bufferedPlayer = null;
1775
+ }
1776
+
1777
+ isDualStreamMode = false;
1778
+
1779
+ // Clean up native player on executor
1780
+ if (pid != 0) {
1781
+ synchronized (nativeLoadLock) {
1782
+ if (nativeLoadExecutor != null && !nativeLoadExecutor.isShutdown()) {
1783
+ nativeLoadExecutor.submit(() -> {
1784
+ WebMAlphaPlayerNative.nativeDestroy(pid);
1785
+ logV("Native player destroyed: " + pid);
1786
+ });
1787
+ nativeLoadExecutor.shutdown();
1788
+ } else {
1789
+ new Thread(() -> {
1790
+ WebMAlphaPlayerNative.nativeDestroy(pid);
1791
+ logV("Native player destroyed (bg): " + pid);
1792
+ }, "WebMDestroy").start();
1793
+ }
1794
+ }
1795
+ }
1796
+
1797
+ // Shutdown render thread, freeing EGL on it first: the surface going away later can't
1798
+ if (onRenderThread(this::destroyEGLSafely)) {
1799
+ renderThread.shutdown();
1800
+ }
1801
+ }
1802
+
1803
+ private void softRelease() {
1804
+ logV("Soft release of player " + playerId + " (dualStream=" + isDualStreamMode + ")");
1805
+
1806
+ cancelNativeLoad();
1807
+
1808
+ synchronized (frameDataLock) {
1809
+ isReleased = true;
1810
+ hasNewFrame = false;
1811
+ pendingY = pendingU = pendingV = pendingA = null;
1812
+ currentWidth = 0;
1813
+ currentHeight = 0;
1814
+ }
1815
+
1816
+ // Soft release buffered dual-stream player
1817
+ if (isDualStreamMode && bufferedPlayer != null) {
1818
+ bufferedPlayer.stop();
1819
+ bufferedPlayer.unload();
1820
+ bufferedPlayer = null;
1821
+ logV("Soft release cleanup complete for buffered dual-stream player");
1822
+ }
1823
+
1824
+ final long pid = playerId;
1825
+ if (pid != 0) {
1826
+ synchronized (nativeLoadLock) {
1827
+ if (nativeLoadExecutor == null || nativeLoadExecutor.isShutdown()) {
1828
+ nativeLoadExecutor = createLoadExecutor();
1829
+ }
1830
+ // Track this task so we can wait for it before reloading
1831
+ softReleaseTask = nativeLoadExecutor.submit(() -> {
1832
+ WebMAlphaPlayerNative.nativeStop(pid);
1833
+ WebMAlphaPlayerNative.nativeUnload(pid);
1834
+ logV("Soft release cleanup complete for player " + pid);
1835
+ });
1836
+ }
1837
+ }
1838
+ }
1839
+
1840
+ /**
1841
+ * Wait for any pending soft release to complete.
1842
+ * Must be called before reloading after a detach/reattach cycle.
1843
+ */
1844
+ private void waitForSoftRelease() {
1845
+ Future<?> task = softReleaseTask;
1846
+ if (task != null && !task.isDone()) {
1847
+ try {
1848
+ logV("Waiting for soft release to complete...");
1849
+ task.get(1000, java.util.concurrent.TimeUnit.MILLISECONDS);
1850
+ logV("Soft release completed");
1851
+ } catch (Exception e) {
1852
+ Log.w(TAG, "Soft release wait interrupted or timed out: " + e.getMessage());
1853
+ }
1854
+ }
1855
+ softReleaseTask = null;
1856
+ }
1857
+
1858
+ @Override
1859
+ protected void onAttachedToWindow() {
1860
+ super.onAttachedToWindow();
1861
+ logV("View attached (isReleased=" + isReleased + ", playerId=" + playerId +
1862
+ ", dualStream=" + isDualStreamMode + ")");
1863
+
1864
+ // If we were previously released, reset the flag so the surface callback can re-load.
1865
+ // This covers both software decode (playerId != 0) and dual-stream (isDualStreamMode)
1866
+ // re-attach after z-index reordering.
1867
+ if (isReleased && (playerId != 0 || isDualStreamMode || sourceUri != null)) {
1868
+ isReleased = false;
1869
+
1870
+ if (renderThread == null || renderThread.isShutdown()) {
1871
+ renderThread = createRenderThread();
1872
+ }
1873
+ }
1874
+ }
1875
+
1876
+ @Override
1877
+ protected void onDetachedFromWindow() {
1878
+ super.onDetachedFromWindow();
1879
+ logV("View detached");
1880
+ softRelease();
1881
+ }
1882
+
1883
+ //------------------------------------------------------------------------------
1884
+ // Helpers
1885
+ //------------------------------------------------------------------------------
1886
+
1887
+ /** Native load failures report themselves through {@link #onError}; only I/O errors are thrown. */
1888
+ private interface NativeLoadCommand {
1889
+ boolean run() throws IOException;
1890
+ }
1891
+
1892
+ /**
1893
+ * Get or create the global decode queue.
1894
+ * This queue serializes non-stream decoding across all player instances.
1895
+ */
1896
+ private static ExecutorService getGlobalDecodeQueue() {
1897
+ synchronized (globalDecodeQueueLock) {
1898
+ if (globalDecodeQueue == null || globalDecodeQueue.isShutdown()) {
1899
+ globalDecodeQueue = Executors.newSingleThreadExecutor(r -> {
1900
+ Thread t = new Thread(r, "WebMGlobalDecode");
1901
+ t.setPriority(Thread.NORM_PRIORITY);
1902
+ return t;
1903
+ });
1904
+ }
1905
+ globalDecodeQueueRefCount++;
1906
+ return globalDecodeQueue;
1907
+ }
1908
+ }
1909
+
1910
+ /**
1911
+ * Release reference to global decode queue.
1912
+ * Queue is shut down when all references are released.
1913
+ */
1914
+ private static void releaseGlobalDecodeQueue() {
1915
+ synchronized (globalDecodeQueueLock) {
1916
+ globalDecodeQueueRefCount--;
1917
+ if (globalDecodeQueueRefCount <= 0 && globalDecodeQueue != null) {
1918
+ globalDecodeQueue.shutdown();
1919
+ globalDecodeQueue = null;
1920
+ globalDecodeQueueRefCount = 0;
1921
+ }
1922
+ }
1923
+ }
1924
+
1925
+ private void enqueueNativeLoad(NativeLoadCommand command) {
1926
+ synchronized (nativeLoadLock) {
1927
+ if (isReleased || playerId == 0) {
1928
+ Log.w(TAG, "Ignoring load - player released");
1929
+ return;
1930
+ }
1931
+
1932
+ // Decide which executor to use:
1933
+ // - Stream mode: use per-instance executor (streaming is already lightweight)
1934
+ // - Preload/StreamAndPreload + useDecodeQueue: use global queue to serialize decoding
1935
+ // - Preload/StreamAndPreload + !useDecodeQueue: use per-instance executor
1936
+ ExecutorService executor;
1937
+ boolean usingGlobalQueue = !MODE_STREAM.equals(mode) && useDecodeQueue;
1938
+
1939
+ if (usingGlobalQueue) {
1940
+ executor = getGlobalDecodeQueue();
1941
+ } else {
1942
+ if (nativeLoadExecutor == null || nativeLoadExecutor.isShutdown()) {
1943
+ nativeLoadExecutor = createLoadExecutor();
1944
+ }
1945
+ executor = nativeLoadExecutor;
1946
+ }
1947
+
1948
+ cancelNativeLoadLocked();
1949
+ final long pid = playerId;
1950
+ final boolean usedGlobalQueue = usingGlobalQueue;
1951
+ currentLoadTask = executor.submit(() -> {
1952
+ try {
1953
+ if (isReleased || playerId != pid) {
1954
+ Log.w(TAG, "Load aborted - player released or changed");
1955
+ return;
1956
+ }
1957
+ command.run();
1958
+ } catch (IOException e) {
1959
+ post(() -> reportError(e.getMessage()));
1960
+ } finally {
1961
+ // Release global queue reference when done
1962
+ if (usedGlobalQueue) {
1963
+ releaseGlobalDecodeQueue();
1964
+ }
1965
+ }
1966
+ });
1967
+ }
1968
+ }
1969
+
1970
+ private void cancelNativeLoad() {
1971
+ synchronized (nativeLoadLock) {
1972
+ cancelNativeLoadLocked();
1973
+ }
1974
+ }
1975
+
1976
+ private void cancelNativeLoadLocked() {
1977
+ if (currentLoadTask != null) {
1978
+ currentLoadTask.cancel(true);
1979
+ currentLoadTask = null;
1980
+ }
1981
+ }
1982
+ }