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