react-native-skia-webgpu-video 0.0.0-stage → 0.1.0-beta.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 +22 -0
- package/README.md +303 -2
- package/android/BUILD_VALIDATION.md +62 -0
- package/android/CMakeLists.txt +99 -0
- package/android/build-validation.json +262 -0
- package/android/build.gradle +185 -0
- package/android/cpp/EncoderTargets.cpp +226 -0
- package/android/cpp/EncoderTargets.h +70 -0
- package/android/cpp/JNIHelpers.cpp +15 -0
- package/android/cpp/JNIHelpers.h +20 -0
- package/android/cpp/NativeEventDispatcher.cpp +59 -0
- package/android/cpp/NativeEventDispatcher.h +31 -0
- package/android/cpp/NativeHardwareBuffer.cpp +397 -0
- package/android/cpp/NativeHardwareBuffer.h +34 -0
- package/android/cpp/NativeRgbaBuffer.cpp +45 -0
- package/android/cpp/NativeRgbaBuffer.h +9 -0
- package/android/cpp/ReturnedVideoFrames.h +37 -0
- package/android/cpp/RgbaFrameLease.h +53 -0
- package/android/cpp/VideoCapabilities.cpp +87 -0
- package/android/cpp/VideoCapabilities.h +44 -0
- package/android/cpp/VideoComposition.cpp +170 -0
- package/android/cpp/VideoComposition.h +36 -0
- package/android/cpp/VideoCompositionFramesExtractor.cpp +82 -0
- package/android/cpp/VideoCompositionFramesExtractor.h +48 -0
- package/android/cpp/VideoCompositionFramesExtractorHostObject.cpp +230 -0
- package/android/cpp/VideoCompositionFramesExtractorHostObject.h +42 -0
- package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.cpp +133 -0
- package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.h +46 -0
- package/android/cpp/VideoEncoderHostObject.cpp +318 -0
- package/android/cpp/VideoEncoderHostObject.h +76 -0
- package/android/cpp/VideoFrame.cpp +293 -0
- package/android/cpp/VideoFrame.h +27 -0
- package/android/cpp/VideoPlayer.cpp +96 -0
- package/android/cpp/VideoPlayer.h +52 -0
- package/android/cpp/VideoPlayerHostObject.cpp +225 -0
- package/android/cpp/VideoPlayerHostObject.h +43 -0
- package/android/cpp/cpp-adapter.cpp +316 -0
- package/android/gradle.properties +5 -0
- package/android/instrumentation-inventory.json +178 -0
- package/android/proguard-rules.pro +4 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/azzapp/rnskv/AudioCompositionExporter.java +537 -0
- package/android/src/main/java/com/azzapp/rnskv/AudioCompositionPlayer.java +321 -0
- package/android/src/main/java/com/azzapp/rnskv/BufferLifetime.java +55 -0
- package/android/src/main/java/com/azzapp/rnskv/DecodeRequests.java +100 -0
- package/android/src/main/java/com/azzapp/rnskv/DecoderWindow.java +55 -0
- package/android/src/main/java/com/azzapp/rnskv/EGLResourcesHolder.java +132 -0
- package/android/src/main/java/com/azzapp/rnskv/EGLUtils.java +225 -0
- package/android/src/main/java/com/azzapp/rnskv/FrameSize.java +50 -0
- package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +510 -0
- package/android/src/main/java/com/azzapp/rnskv/JNIHelpers.java +12 -0
- package/android/src/main/java/com/azzapp/rnskv/NativeEventDispatcher.java +25 -0
- package/android/src/main/java/com/azzapp/rnskv/NativeHardwareBuffer.java +62 -0
- package/android/src/main/java/com/azzapp/rnskv/NativeRgbaBuffer.java +35 -0
- package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoModule.java +45 -0
- package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoPackage.java +43 -0
- package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoSpec.java +9 -0
- package/android/src/main/java/com/azzapp/rnskv/RgbaBufferAllocator.java +83 -0
- package/android/src/main/java/com/azzapp/rnskv/RgbaLayout.java +33 -0
- package/android/src/main/java/com/azzapp/rnskv/SurfaceOutputPolicy.java +13 -0
- package/android/src/main/java/com/azzapp/rnskv/TextureRenderer.java +150 -0
- package/android/src/main/java/com/azzapp/rnskv/TimeHelpers.java +11 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCapabilities.java +354 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +147 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +772 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +354 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractorSync.java +241 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionItemDecoder.java +492 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +639 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoFrame.java +74 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoPlayer.java +361 -0
- package/cpp/RNSVCheckedSizes.h +54 -0
- package/cpp/RNSVEventEmitter.cpp +199 -0
- package/cpp/RNSVEventEmitter.h +31 -0
- package/cpp/RNSVHostObject.cpp +79 -0
- package/cpp/RNSVHostObject.h +67 -0
- package/cpp/RNSVMemoryBudget.cpp +68 -0
- package/cpp/RNSVMemoryBudget.h +100 -0
- package/cpp/RNSVRuntimeLifecycleMonitor.cpp +88 -0
- package/cpp/RNSVRuntimeLifecycleMonitor.h +33 -0
- package/docs/ANDROID_DEVICE_306.md +95 -0
- package/docs/ANDROID_EXPORT_CACHE_306.md +48 -0
- package/docs/ANDROID_MEMORY_RETENTION_306.md +67 -0
- package/docs/BENCHMARKS.md +368 -0
- package/docs/IOS_DEVICE_TESTS.md +115 -0
- package/docs/IOS_SIMULATOR_TESTS.md +74 -0
- package/docs/MIGRATION.md +82 -0
- package/docs/NATIVE_MEMORY_INVESTIGATION_306.md +88 -0
- package/docs/PERFORMANCE_AB_IPHONE_303.md +196 -0
- package/docs/PERFORMANCE_AB_SIMULATOR.md +135 -0
- package/docs/PERFORMANCE_AB_SOAK_SIMULATOR.md +84 -0
- package/docs/PERFORMANCE_IPHONE_306_OPTIMIZATION.md +76 -0
- package/docs/PERFORMANCE_SIMULATOR.md +89 -0
- package/docs/PERFORMANCE_SIMULATOR_INITIAL.md +96 -0
- package/docs/PERFORMANCE_SOAK_IPHONE_303.md +41 -0
- package/docs/UPSTREAM_NOTES.md +188 -0
- package/docs/assets/banner.svg +26 -0
- package/examples/webgpu-overlay.ts +147 -0
- package/ios/AVAssetTrackUtils.h +24 -0
- package/ios/AudioCompositionUtils.h +27 -0
- package/ios/AudioCompositionUtils.mm +113 -0
- package/ios/DecoderWindow.h +52 -0
- package/ios/NativeResourceStats.h +32 -0
- package/ios/RNSVColorSpace.h +50 -0
- package/ios/RNSVJSIUtils.h +46 -0
- package/ios/RNSVScopedPixelBuffer.h +17 -0
- package/ios/RNSVVideoPlayer.h +46 -0
- package/ios/RNSVVideoPlayer.mm +410 -0
- package/ios/ReactNativeSkiaVideo.h +8 -0
- package/ios/ReactNativeSkiaVideo.mm +310 -0
- package/ios/SeekPolicy.h +27 -0
- package/ios/VideoComposition.h +148 -0
- package/ios/VideoCompositionFramesExtractorHostObject.h +105 -0
- package/ios/VideoCompositionFramesExtractorHostObject.mm +686 -0
- package/ios/VideoCompositionFramesExtractorSyncHostObject.h +37 -0
- package/ios/VideoCompositionFramesExtractorSyncHostObject.mm +168 -0
- package/ios/VideoCompositionItemDecoder.h +60 -0
- package/ios/VideoCompositionItemDecoder.mm +480 -0
- package/ios/VideoEncoderHostObject.h +83 -0
- package/ios/VideoEncoderHostObject.mm +706 -0
- package/ios/VideoFrame.h +66 -0
- package/ios/VideoFrame.mm +108 -0
- package/ios/VideoPlayerHostObject.h +52 -0
- package/ios/VideoPlayerHostObject.mm +332 -0
- package/lib/module/NativeReactNativeSkiaVideo.js +5 -0
- package/lib/module/NativeReactNativeSkiaVideo.js.map +1 -0
- package/lib/module/RNSkiaVideoModule.js +20 -0
- package/lib/module/RNSkiaVideoModule.js.map +1 -0
- package/lib/module/canvas.js +18 -0
- package/lib/module/canvas.js.map +1 -0
- package/lib/module/exportCache.js +22 -0
- package/lib/module/exportCache.js.map +1 -0
- package/lib/module/exportFrames.js +338 -0
- package/lib/module/exportFrames.js.map +1 -0
- package/lib/module/exportQueue.js +52 -0
- package/lib/module/exportQueue.js.map +1 -0
- package/lib/module/exportTarget.js +137 -0
- package/lib/module/exportTarget.js.map +1 -0
- package/lib/module/exportVideoComposition.js +492 -0
- package/lib/module/exportVideoComposition.js.map +1 -0
- package/lib/module/frameInterop.js +532 -0
- package/lib/module/frameInterop.js.map +1 -0
- package/lib/module/gpu.js +222 -0
- package/lib/module/gpu.js.map +1 -0
- package/lib/module/gpuDevice.js +54 -0
- package/lib/module/gpuDevice.js.map +1 -0
- package/lib/module/index.js +36 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/memory.js +38 -0
- package/lib/module/memory.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/previewLifecycle.js +52 -0
- package/lib/module/previewLifecycle.js.map +1 -0
- package/lib/module/runtimeYield.js +10 -0
- package/lib/module/runtimeYield.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/useEventListener.js +18 -0
- package/lib/module/utils/useEventListener.js.map +1 -0
- package/lib/module/videoCompositionPlayer.js +348 -0
- package/lib/module/videoCompositionPlayer.js.map +1 -0
- package/lib/module/videoFrameImage.js +185 -0
- package/lib/module/videoFrameImage.js.map +1 -0
- package/lib/module/videoPlayer.js +172 -0
- package/lib/module/videoPlayer.js.map +1 -0
- package/lib/module/yuvBlit.js +299 -0
- package/lib/module/yuvBlit.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts +7 -0
- package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts.map +1 -0
- package/lib/typescript/src/RNSkiaVideoModule.d.ts +4 -0
- package/lib/typescript/src/RNSkiaVideoModule.d.ts.map +1 -0
- package/lib/typescript/src/canvas.d.ts +4 -0
- package/lib/typescript/src/canvas.d.ts.map +1 -0
- package/lib/typescript/src/exportCache.d.ts +6 -0
- package/lib/typescript/src/exportCache.d.ts.map +1 -0
- package/lib/typescript/src/exportFrames.d.ts +57 -0
- package/lib/typescript/src/exportFrames.d.ts.map +1 -0
- package/lib/typescript/src/exportQueue.d.ts +5 -0
- package/lib/typescript/src/exportQueue.d.ts.map +1 -0
- package/lib/typescript/src/exportTarget.d.ts +28 -0
- package/lib/typescript/src/exportTarget.d.ts.map +1 -0
- package/lib/typescript/src/exportVideoComposition.d.ts +68 -0
- package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -0
- package/lib/typescript/src/frameInterop.d.ts +17 -0
- package/lib/typescript/src/frameInterop.d.ts.map +1 -0
- package/lib/typescript/src/gpu.d.ts +39 -0
- package/lib/typescript/src/gpu.d.ts.map +1 -0
- package/lib/typescript/src/gpuDevice.d.ts +11 -0
- package/lib/typescript/src/gpuDevice.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +21 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/memory.d.ts +25 -0
- package/lib/typescript/src/memory.d.ts.map +1 -0
- package/lib/typescript/src/previewLifecycle.d.ts +22 -0
- package/lib/typescript/src/previewLifecycle.d.ts.map +1 -0
- package/lib/typescript/src/runtimeYield.d.ts +4 -0
- package/lib/typescript/src/runtimeYield.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +631 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/useEventListener.d.ts +5 -0
- package/lib/typescript/src/utils/useEventListener.d.ts.map +1 -0
- package/lib/typescript/src/videoCompositionPlayer.d.ts +85 -0
- package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -0
- package/lib/typescript/src/videoFrameImage.d.ts +60 -0
- package/lib/typescript/src/videoFrameImage.d.ts.map +1 -0
- package/lib/typescript/src/videoPlayer.d.ts +99 -0
- package/lib/typescript/src/videoPlayer.d.ts.map +1 -0
- package/lib/typescript/src/yuvBlit.d.ts +50 -0
- package/lib/typescript/src/yuvBlit.d.ts.map +1 -0
- package/package.json +154 -4
- package/patches/react-native-skia+3.7.0.patch +502 -0
- package/react-native-skia-webgpu-video.podspec +35 -0
- package/scripts/apply-skia-canvas-patch.mjs +48 -0
- package/scripts/skia-canvas-patch.json +291 -0
- package/src/NativeReactNativeSkiaVideo.ts +8 -0
- package/src/RNSkiaVideoModule.ts +27 -0
- package/src/canvas.ts +22 -0
- package/src/exportCache.ts +27 -0
- package/src/exportFrames.ts +421 -0
- package/src/exportQueue.ts +65 -0
- package/src/exportTarget.ts +179 -0
- package/src/exportVideoComposition.ts +718 -0
- package/src/frameInterop.ts +684 -0
- package/src/gpu.ts +290 -0
- package/src/gpuDevice.ts +80 -0
- package/src/index.tsx +45 -0
- package/src/memory.ts +48 -0
- package/src/previewLifecycle.ts +60 -0
- package/src/runtimeYield.ts +6 -0
- package/src/types.ts +661 -0
- package/src/utils/useEventListener.ts +22 -0
- package/src/videoCompositionPlayer.ts +520 -0
- package/src/videoFrameImage.ts +205 -0
- package/src/videoPlayer.ts +318 -0
- package/src/yuvBlit.ts +331 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#import <CoreVideo/CoreVideo.h>
|
|
3
|
+
#include <atomic>
|
|
4
|
+
#include <cstdint>
|
|
5
|
+
namespace RNSkiaVideo {
|
|
6
|
+
struct ResourceCounter {
|
|
7
|
+
std::atomic<uint64_t> current{0};
|
|
8
|
+
std::atomic<uint64_t> peak{0};
|
|
9
|
+
void add(uint64_t amount) {
|
|
10
|
+
uint64_t value = current.fetch_add(amount) + amount;
|
|
11
|
+
uint64_t previous = peak.load();
|
|
12
|
+
while (value > previous && !peak.compare_exchange_weak(previous, value)) {}
|
|
13
|
+
}
|
|
14
|
+
void remove(uint64_t amount) { current.fetch_sub(amount); }
|
|
15
|
+
};
|
|
16
|
+
struct NativeResourceStats {
|
|
17
|
+
inline static ResourceCounter frameBuffers;
|
|
18
|
+
inline static ResourceCounter frameBytes;
|
|
19
|
+
inline static ResourceCounter decoderBuffers;
|
|
20
|
+
inline static ResourceCounter decoderBytes;
|
|
21
|
+
inline static ResourceCounter decoders;
|
|
22
|
+
inline static ResourceCounter encoders;
|
|
23
|
+
// Codec-owned references after append require process/Instruments measures.
|
|
24
|
+
inline static ResourceCounter encoderSubmittingBuffers;
|
|
25
|
+
inline static ResourceCounter encoderSubmittingBytes;
|
|
26
|
+
};
|
|
27
|
+
inline size_t pixelBufferBytes(CVPixelBufferRef buffer) {
|
|
28
|
+
size_t bytes = CVPixelBufferGetDataSize(buffer);
|
|
29
|
+
return bytes ? bytes : CVPixelBufferGetBytesPerRow(buffer) *
|
|
30
|
+
CVPixelBufferGetHeight(buffer);
|
|
31
|
+
}
|
|
32
|
+
} // namespace RNSkiaVideo
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#import <AVFoundation/AVFoundation.h>
|
|
4
|
+
#import <CoreVideo/CoreVideo.h>
|
|
5
|
+
|
|
6
|
+
namespace RNSkiaVideo {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Rec.709 — the one color space this library decodes into, draws in and
|
|
10
|
+
* encodes out.
|
|
11
|
+
*
|
|
12
|
+
* Skia is handed plain BGRA textures and does no color management, so every
|
|
13
|
+
* frame has to reach it already in the space the canvas assumes. Since the
|
|
14
|
+
* iPhone 12 the camera records HLG HDR by default, and an HLG buffer decoded
|
|
15
|
+
* as-is into BGRA and drawn as sRGB comes out washed out, with crushed
|
|
16
|
+
* contrast. Asking AVFoundation for these properties makes it tone-map to
|
|
17
|
+
* SDR on the way out, on its own fast path.
|
|
18
|
+
*
|
|
19
|
+
* Applied unconditionally rather than only to HDR sources: a source already
|
|
20
|
+
* in Rec.709 costs nothing, and a BT.601 one gets the conversion it always
|
|
21
|
+
* needed. It also spares the decoders an inspection of the track's transfer
|
|
22
|
+
* function before its format descriptions are even loaded.
|
|
23
|
+
*/
|
|
24
|
+
static inline NSDictionary* SDRColorProperties() {
|
|
25
|
+
return @{
|
|
26
|
+
AVVideoColorPrimariesKey : AVVideoColorPrimaries_ITU_R_709_2,
|
|
27
|
+
AVVideoTransferFunctionKey : AVVideoTransferFunction_ITU_R_709_2,
|
|
28
|
+
AVVideoYCbCrMatrixKey : AVVideoYCbCrMatrix_ITU_R_709_2,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Stamps Rec.709 onto a buffer handed to AVAssetWriter. The writer's own
|
|
34
|
+
* output settings already declare it, but a buffer carrying no color
|
|
35
|
+
* attachment at all leaves some muxers to guess, and a file with no
|
|
36
|
+
* colorimetry is read differently by every player.
|
|
37
|
+
*/
|
|
38
|
+
static inline void TagBufferAsSDR(CVPixelBufferRef buffer) {
|
|
39
|
+
CVBufferSetAttachment(buffer, kCVImageBufferColorPrimariesKey,
|
|
40
|
+
kCVImageBufferColorPrimaries_ITU_R_709_2,
|
|
41
|
+
kCVAttachmentMode_ShouldPropagate);
|
|
42
|
+
CVBufferSetAttachment(buffer, kCVImageBufferTransferFunctionKey,
|
|
43
|
+
kCVImageBufferTransferFunction_ITU_R_709_2,
|
|
44
|
+
kCVAttachmentMode_ShouldPropagate);
|
|
45
|
+
CVBufferSetAttachment(buffer, kCVImageBufferYCbCrMatrixKey,
|
|
46
|
+
kCVImageBufferYCbCrMatrix_ITU_R_709_2,
|
|
47
|
+
kCVAttachmentMode_ShouldPropagate);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
} // namespace RNSkiaVideo
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#import <Foundation/Foundation.h>
|
|
3
|
+
#import <jsi/jsi.h>
|
|
4
|
+
#include <stdexcept>
|
|
5
|
+
#include <type_traits>
|
|
6
|
+
|
|
7
|
+
namespace RNSkiaVideo {
|
|
8
|
+
|
|
9
|
+
using namespace facebook;
|
|
10
|
+
|
|
11
|
+
// The worklet runtime threads that drive video exports never drain their
|
|
12
|
+
// autorelease pool, so every autoreleased object created by a host function
|
|
13
|
+
// (Metal command buffers, CoreMedia wrappers, AVFoundation internals…) would
|
|
14
|
+
// accumulate for the lifetime of the app. JSI entry points doing ObjC work
|
|
15
|
+
// run inside this helper. Convert NSError into an owning C++ exception after
|
|
16
|
+
// the pool drains, avoiding a leaked CF retain on every failed export.
|
|
17
|
+
template <typename F>
|
|
18
|
+
static jsi::Value runPooled(F&& body) {
|
|
19
|
+
NSError* pendingError = nil;
|
|
20
|
+
jsi::Value result = jsi::Value::undefined();
|
|
21
|
+
@autoreleasepool {
|
|
22
|
+
try {
|
|
23
|
+
if constexpr (std::is_void_v<std::invoke_result_t<F>>) body();
|
|
24
|
+
else result = body();
|
|
25
|
+
} catch (NSError* error) {
|
|
26
|
+
pendingError = error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (pendingError) {
|
|
30
|
+
throw std::runtime_error([[pendingError localizedDescription] UTF8String]);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static jsi::Value NSErrorToJSI(jsi::Runtime& runtime, NSError* error) {
|
|
36
|
+
auto jsError = jsi::Object(runtime);
|
|
37
|
+
auto message = error == nil ? @"Unknown error" : [error description];
|
|
38
|
+
jsError.setProperty(
|
|
39
|
+
runtime, "message",
|
|
40
|
+
jsi::String::createFromUtf8(runtime, [message UTF8String]));
|
|
41
|
+
jsError.setProperty(runtime, "code",
|
|
42
|
+
error != nil ? jsi::Value((double)[error code])
|
|
43
|
+
: jsi::Value::null());
|
|
44
|
+
return jsError;
|
|
45
|
+
}
|
|
46
|
+
} // namespace RNSkiaVideo
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#import <CoreVideo/CoreVideo.h>
|
|
3
|
+
|
|
4
|
+
namespace RNSkiaVideo {
|
|
5
|
+
// Adopts the +1 returned by CoreVideo/AVPlayer copy APIs. The owner always
|
|
6
|
+
// releases it, including when reserving a frame lease throws.
|
|
7
|
+
class ScopedPixelBuffer {
|
|
8
|
+
public:
|
|
9
|
+
explicit ScopedPixelBuffer(CVPixelBufferRef buffer) : buffer(buffer) {}
|
|
10
|
+
~ScopedPixelBuffer() { if (buffer) CVPixelBufferRelease(buffer); }
|
|
11
|
+
ScopedPixelBuffer(const ScopedPixelBuffer&) = delete;
|
|
12
|
+
ScopedPixelBuffer& operator=(const ScopedPixelBuffer&) = delete;
|
|
13
|
+
CVPixelBufferRef get() const { return buffer; }
|
|
14
|
+
private:
|
|
15
|
+
CVPixelBufferRef buffer;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#import <AVFoundation/AVFoundation.h>
|
|
2
|
+
#import <Foundation/Foundation.h>
|
|
3
|
+
#import <Metal/Metal.h>
|
|
4
|
+
|
|
5
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
6
|
+
|
|
7
|
+
@protocol RNSVVideoPlayerDelegate <NSObject>
|
|
8
|
+
|
|
9
|
+
- (void)readyToPlay:(NSDictionary*)assetInfos;
|
|
10
|
+
- (void)frameAvailable:(CMTime)time;
|
|
11
|
+
- (void)bufferingStart;
|
|
12
|
+
- (void)bufferingEnd;
|
|
13
|
+
- (void)bufferingUpdate:(NSArray<NSValue*>*)loadedTimeRanges;
|
|
14
|
+
- (void)videoError:(nullable NSError*)error;
|
|
15
|
+
- (void)complete;
|
|
16
|
+
- (void)isPlaying:(BOOL)playing;
|
|
17
|
+
|
|
18
|
+
@end
|
|
19
|
+
|
|
20
|
+
@interface RNSVVideoPlayer : NSObject
|
|
21
|
+
|
|
22
|
+
@property(nonatomic, readonly) CMTime currentTime;
|
|
23
|
+
@property(nonatomic, readonly) CMTime duration;
|
|
24
|
+
@property(nonatomic) float volume;
|
|
25
|
+
@property(nonatomic) float playbackSpeed;
|
|
26
|
+
@property(nonatomic, readonly) BOOL disposed;
|
|
27
|
+
@property(nonatomic, readonly) BOOL isInitialized;
|
|
28
|
+
@property(nonatomic, readonly) BOOL isPlaying;
|
|
29
|
+
@property(nonatomic) BOOL isLooping;
|
|
30
|
+
@property(nonatomic) CGSize resolution;
|
|
31
|
+
|
|
32
|
+
- (instancetype)initWithURL:(NSURL*)url
|
|
33
|
+
delegate:(id<RNSVVideoPlayerDelegate>)delegate
|
|
34
|
+
resolution:(CGSize)resolution;
|
|
35
|
+
/**
|
|
36
|
+
* Returns the pixel buffer for the given item time, or NULL if none is
|
|
37
|
+
* available. Ownership is transferred to the caller (CVPixelBufferRelease).
|
|
38
|
+
*/
|
|
39
|
+
- (nullable CVPixelBufferRef)copyPixelBufferForTime:(CMTime)time;
|
|
40
|
+
- (void)pause;
|
|
41
|
+
- (void)play;
|
|
42
|
+
- (void)seekTo:(CMTime)time completionHandler:(void (^)(BOOL))completionHandle;
|
|
43
|
+
- (void)dispose;
|
|
44
|
+
@end
|
|
45
|
+
|
|
46
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Port from flutter video player, see:
|
|
3
|
+
// https://github.com/flutter/packages/blob/41f5a16e13f4c9327e75d431d338c33f2fb35870/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin.m
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
#import "RNSVVideoPlayer.h"
|
|
7
|
+
#import "AVAssetTrackUtils.h"
|
|
8
|
+
#import "RNSVColorSpace.h"
|
|
9
|
+
#import <QuartzCore/QuartzCore.h>
|
|
10
|
+
#include <atomic>
|
|
11
|
+
|
|
12
|
+
static void* timeRangeContext = &timeRangeContext;
|
|
13
|
+
static void* statusContext = &statusContext;
|
|
14
|
+
static void* presentationSizeContext = &presentationSizeContext;
|
|
15
|
+
static void* durationContext = &durationContext;
|
|
16
|
+
static void* playbackLikelyToKeepUpContext = &playbackLikelyToKeepUpContext;
|
|
17
|
+
static void* rateContext = &rateContext;
|
|
18
|
+
|
|
19
|
+
@implementation RNSVVideoPlayer {
|
|
20
|
+
AVPlayer* _player;
|
|
21
|
+
AVPlayerItemVideoOutput* _videoOutput;
|
|
22
|
+
CADisplayLink* _displayLink;
|
|
23
|
+
id<RNSVVideoPlayerDelegate> _delegate;
|
|
24
|
+
Boolean _complete;
|
|
25
|
+
Boolean _waitingForFrame;
|
|
26
|
+
float _playbackSpeed;
|
|
27
|
+
std::atomic<bool> _disposed;
|
|
28
|
+
std::atomic<bool> _tracksLoading;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
- (instancetype)initWithURL:(NSURL*)url
|
|
32
|
+
delegate:(id<RNSVVideoPlayerDelegate>)delegate
|
|
33
|
+
resolution:(CGSize)resolution {
|
|
34
|
+
self = [super init];
|
|
35
|
+
_disposed = false;
|
|
36
|
+
_tracksLoading = false;
|
|
37
|
+
_delegate = delegate;
|
|
38
|
+
_complete = NO;
|
|
39
|
+
_waitingForFrame = YES;
|
|
40
|
+
_playbackSpeed = 1.0f;
|
|
41
|
+
|
|
42
|
+
AVAsset* asset = [AVAsset assetWithURL:url];
|
|
43
|
+
self.resolution = resolution;
|
|
44
|
+
// IOSurface-backed: the frames are wrapped into Metal texture views without
|
|
45
|
+
// a copy, and their recycling is gated on the surface's use count.
|
|
46
|
+
// Output settings rather than plain pixel buffer attributes: the dictionary
|
|
47
|
+
// takes both, and only this form carries the color properties that make
|
|
48
|
+
// AVFoundation tone-map an HDR source to Rec.709 (see RNSVColorSpace.h).
|
|
49
|
+
NSMutableDictionary* outputSettings = [@{
|
|
50
|
+
(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
|
|
51
|
+
(id)kCVPixelBufferIOSurfacePropertiesKey : @{},
|
|
52
|
+
(id)kCVPixelBufferMetalCompatibilityKey : @YES,
|
|
53
|
+
AVVideoColorPropertiesKey : RNSkiaVideo::SDRColorProperties(),
|
|
54
|
+
} mutableCopy];
|
|
55
|
+
if (!CGSizeEqualToSize(CGSizeZero, resolution)) {
|
|
56
|
+
[outputSettings setValue:@(resolution.width)
|
|
57
|
+
forKey:(id)kCVPixelBufferWidthKey];
|
|
58
|
+
[outputSettings setValue:@(resolution.height)
|
|
59
|
+
forKey:(id)kCVPixelBufferHeightKey];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_videoOutput =
|
|
63
|
+
[[AVPlayerItemVideoOutput alloc] initWithOutputSettings:outputSettings];
|
|
64
|
+
|
|
65
|
+
_displayLink =
|
|
66
|
+
[CADisplayLink displayLinkWithTarget:self
|
|
67
|
+
selector:@selector(displayLinkFired:)];
|
|
68
|
+
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop]
|
|
69
|
+
forMode:NSRunLoopCommonModes];
|
|
70
|
+
|
|
71
|
+
AVPlayerItem* item = [AVPlayerItem playerItemWithAsset:asset];
|
|
72
|
+
_player = [AVPlayer playerWithPlayerItem:item];
|
|
73
|
+
_player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
|
|
74
|
+
[self addObserversForItem:item player:_player];
|
|
75
|
+
|
|
76
|
+
return self;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
- (BOOL)disposed { return _disposed.load(); }
|
|
80
|
+
|
|
81
|
+
- (CMTime)currentTime {
|
|
82
|
+
return _player.currentTime;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
- (CMTime)duration {
|
|
86
|
+
return _player.currentItem.duration;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
- (void)pause {
|
|
90
|
+
_isPlaying = NO;
|
|
91
|
+
[self updatePlayingState];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
- (void)play {
|
|
95
|
+
_isPlaying = YES;
|
|
96
|
+
[self updatePlayingState];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
- (void)setIsLooping:(BOOL)isLooping {
|
|
100
|
+
_isLooping = isLooping;
|
|
101
|
+
_player.actionAtItemEnd =
|
|
102
|
+
isLooping ? AVPlayerActionAtItemEndNone : AVPlayerActionAtItemEndPause;
|
|
103
|
+
// restart only if video as ended
|
|
104
|
+
if (_complete) {
|
|
105
|
+
_complete = NO;
|
|
106
|
+
[_player.currentItem seekToTime:kCMTimeZero completionHandler:nil];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
- (float)volume {
|
|
111
|
+
return _player.volume;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
- (void)setVolume:(float)volume {
|
|
115
|
+
_player.volume =
|
|
116
|
+
(float)((volume < 0.0) ? 0.0 : ((volume > 1.0) ? 1.0 : volume));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
- (float)playbackSpeed {
|
|
120
|
+
return _playbackSpeed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
- (void)setPlaybackSpeed:(float)playbackSpeed {
|
|
124
|
+
_playbackSpeed = MAX(0.1f, playbackSpeed);
|
|
125
|
+
if (_isPlaying) {
|
|
126
|
+
_player.rate = _playbackSpeed;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
- (nullable CVPixelBufferRef)copyPixelBufferForTime:(CMTime)time {
|
|
131
|
+
if ([_videoOutput hasNewPixelBufferForItemTime:time]) {
|
|
132
|
+
// The frame retains this immutable buffer until its explicit disposal.
|
|
133
|
+
return [_videoOutput copyPixelBufferForItemTime:time
|
|
134
|
+
itemTimeForDisplay:nil];
|
|
135
|
+
}
|
|
136
|
+
return NULL;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
- (void)seekTo:(CMTime)time
|
|
140
|
+
completionHandler:(void (^)(BOOL))completionHandler {
|
|
141
|
+
CMTime previousCMTime = _player.currentTime;
|
|
142
|
+
CMTime duration = _player.currentItem.asset.duration;
|
|
143
|
+
CMTime tolerance =
|
|
144
|
+
duration.value == time.value ? CMTimeMake(1, 1000) : kCMTimeZero;
|
|
145
|
+
__weak RNSVVideoPlayer* weakSelf = self;
|
|
146
|
+
[_player seekToTime:time
|
|
147
|
+
toleranceBefore:tolerance
|
|
148
|
+
toleranceAfter:tolerance
|
|
149
|
+
completionHandler:^(BOOL completed) {
|
|
150
|
+
RNSVVideoPlayer* owner = weakSelf;
|
|
151
|
+
if (!owner || owner->_disposed) return;
|
|
152
|
+
if (CMTimeCompare(owner->_player.currentTime, previousCMTime) != 0) {
|
|
153
|
+
// Ensure that a frame is drawn once available, even if currently
|
|
154
|
+
// paused. In theory a race is possible here where the new frame has
|
|
155
|
+
// already drawn by the time this code runs, and the display link
|
|
156
|
+
// stays on indefinitely, but that should be relatively harmless. This
|
|
157
|
+
// must use the display link rather than just informing the engine
|
|
158
|
+
// that a new frame is available because the seek completing doesn't
|
|
159
|
+
// guarantee that the pixel buffer is already available.
|
|
160
|
+
[owner expectFrame];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (completionHandler) {
|
|
164
|
+
completionHandler(completed);
|
|
165
|
+
}
|
|
166
|
+
}];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
- (void)addObserversForItem:(AVPlayerItem*)item player:(AVPlayer*)player {
|
|
170
|
+
[item addObserver:self
|
|
171
|
+
forKeyPath:@"loadedTimeRanges"
|
|
172
|
+
options:NSKeyValueObservingOptionInitial |
|
|
173
|
+
NSKeyValueObservingOptionNew
|
|
174
|
+
context:timeRangeContext];
|
|
175
|
+
[item addObserver:self
|
|
176
|
+
forKeyPath:@"status"
|
|
177
|
+
options:NSKeyValueObservingOptionInitial |
|
|
178
|
+
NSKeyValueObservingOptionNew
|
|
179
|
+
context:statusContext];
|
|
180
|
+
[item addObserver:self
|
|
181
|
+
forKeyPath:@"presentationSize"
|
|
182
|
+
options:NSKeyValueObservingOptionInitial |
|
|
183
|
+
NSKeyValueObservingOptionNew
|
|
184
|
+
context:presentationSizeContext];
|
|
185
|
+
[item addObserver:self
|
|
186
|
+
forKeyPath:@"duration"
|
|
187
|
+
options:NSKeyValueObservingOptionInitial |
|
|
188
|
+
NSKeyValueObservingOptionNew
|
|
189
|
+
context:durationContext];
|
|
190
|
+
[item addObserver:self
|
|
191
|
+
forKeyPath:@"playbackLikelyToKeepUp"
|
|
192
|
+
options:NSKeyValueObservingOptionInitial |
|
|
193
|
+
NSKeyValueObservingOptionNew
|
|
194
|
+
context:playbackLikelyToKeepUpContext];
|
|
195
|
+
|
|
196
|
+
// Add observer to AVPlayer instead of AVPlayerItem since the AVPlayerItem
|
|
197
|
+
// does not have a "rate" property
|
|
198
|
+
[player addObserver:self
|
|
199
|
+
forKeyPath:@"rate"
|
|
200
|
+
options:NSKeyValueObservingOptionInitial |
|
|
201
|
+
NSKeyValueObservingOptionNew
|
|
202
|
+
context:rateContext];
|
|
203
|
+
|
|
204
|
+
[[NSNotificationCenter defaultCenter]
|
|
205
|
+
addObserver:self
|
|
206
|
+
selector:@selector(itemDidPlayToEndTime:)
|
|
207
|
+
name:AVPlayerItemDidPlayToEndTimeNotification
|
|
208
|
+
object:item];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
- (void)displayLinkFired:(CADisplayLink*)sender {
|
|
212
|
+
if (_disposed) return;
|
|
213
|
+
CMTime outputItemTime =
|
|
214
|
+
[_videoOutput itemTimeForHostTime:CACurrentMediaTime()];
|
|
215
|
+
if ([_videoOutput hasNewPixelBufferForItemTime:outputItemTime]) {
|
|
216
|
+
[_delegate frameAvailable:outputItemTime];
|
|
217
|
+
if (_waitingForFrame) {
|
|
218
|
+
_waitingForFrame = NO;
|
|
219
|
+
// If the display link was only running temporarily to pick up a new frame
|
|
220
|
+
// while the video was paused, stop it again.
|
|
221
|
+
if (!self.isPlaying) {
|
|
222
|
+
_displayLink.paused = YES;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
- (void)itemDidPlayToEndTime:(NSNotification*)notification {
|
|
229
|
+
if (_disposed) return;
|
|
230
|
+
if (notification.object == _player.currentItem && _isLooping) {
|
|
231
|
+
AVPlayerItem* item = [notification object];
|
|
232
|
+
// [item seekToTime:kCMTimeZero completionHandler:nil];
|
|
233
|
+
__weak RNSVVideoPlayer* weakSelf = self;
|
|
234
|
+
[item seekToTime:kCMTimeZero
|
|
235
|
+
toleranceBefore:kCMTimeZero
|
|
236
|
+
toleranceAfter:kCMTimeZero
|
|
237
|
+
completionHandler:^(BOOL finished) {
|
|
238
|
+
__strong RNSVVideoPlayer* strongSelf = weakSelf;
|
|
239
|
+
if (!strongSelf || strongSelf->_disposed)
|
|
240
|
+
return;
|
|
241
|
+
if (finished) {
|
|
242
|
+
[strongSelf->_delegate frameAvailable:kCMTimeZero];
|
|
243
|
+
}
|
|
244
|
+
}];
|
|
245
|
+
} else {
|
|
246
|
+
[_delegate complete];
|
|
247
|
+
_complete = YES;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
- (void)observeValueForKeyPath:(NSString*)path
|
|
252
|
+
ofObject:(id)object
|
|
253
|
+
change:(NSDictionary*)change
|
|
254
|
+
context:(void*)context {
|
|
255
|
+
if (_disposed) return;
|
|
256
|
+
if (context == timeRangeContext) {
|
|
257
|
+
NSArray<NSValue*>* timeRanges = [object loadedTimeRanges];
|
|
258
|
+
if (timeRanges != nil) {
|
|
259
|
+
[_delegate bufferingUpdate:[object loadedTimeRanges]];
|
|
260
|
+
}
|
|
261
|
+
} else if (context == statusContext) {
|
|
262
|
+
AVPlayerItem* item = (AVPlayerItem*)object;
|
|
263
|
+
switch (item.status) {
|
|
264
|
+
case AVPlayerItemStatusFailed:
|
|
265
|
+
[_delegate videoError:item.error];
|
|
266
|
+
break;
|
|
267
|
+
case AVPlayerItemStatusUnknown:
|
|
268
|
+
break;
|
|
269
|
+
case AVPlayerItemStatusReadyToPlay:
|
|
270
|
+
[item addOutput:_videoOutput];
|
|
271
|
+
[self dispatchReadyToPlay];
|
|
272
|
+
[self updatePlayingState];
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
} else if (context == presentationSizeContext || context == durationContext) {
|
|
276
|
+
AVPlayerItem* item = (AVPlayerItem*)object;
|
|
277
|
+
if (item.status == AVPlayerItemStatusReadyToPlay) {
|
|
278
|
+
// Due to an apparent bug, when the player item is ready, it still may not
|
|
279
|
+
// have determined its presentation size or duration. When these
|
|
280
|
+
// properties are finally set, re-check if all required properties and
|
|
281
|
+
// instantiate the event sink if it is not already set up.
|
|
282
|
+
[self dispatchReadyToPlay];
|
|
283
|
+
[self updatePlayingState];
|
|
284
|
+
}
|
|
285
|
+
} else if (context == playbackLikelyToKeepUpContext) {
|
|
286
|
+
[self updatePlayingState];
|
|
287
|
+
if ([[_player currentItem] isPlaybackLikelyToKeepUp]) {
|
|
288
|
+
[_delegate bufferingEnd];
|
|
289
|
+
} else {
|
|
290
|
+
[_delegate bufferingStart];
|
|
291
|
+
}
|
|
292
|
+
} else if (context == rateContext) {
|
|
293
|
+
AVPlayer* player = (AVPlayer*)object;
|
|
294
|
+
[_delegate isPlaying:player.rate > 0];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
- (void)dispatchReadyToPlay {
|
|
299
|
+
if (_disposed) return;
|
|
300
|
+
if (!_isInitialized) {
|
|
301
|
+
AVPlayerItem* currentItem = _player.currentItem;
|
|
302
|
+
CGSize resolution = CGSizeEqualToSize(self.resolution, CGSizeZero)
|
|
303
|
+
? currentItem.presentationSize
|
|
304
|
+
: self.resolution;
|
|
305
|
+
CGFloat width = resolution.width;
|
|
306
|
+
CGFloat height = resolution.height;
|
|
307
|
+
|
|
308
|
+
// Wait until tracks are loaded to check duration or if there are any
|
|
309
|
+
// videos.
|
|
310
|
+
AVAsset* asset = currentItem.asset;
|
|
311
|
+
if ([asset statusOfValueForKey:@"tracks"
|
|
312
|
+
error:nil] != AVKeyValueStatusLoaded) {
|
|
313
|
+
if (_tracksLoading.exchange(true)) return;
|
|
314
|
+
__weak RNSVVideoPlayer* weakSelf = self;
|
|
315
|
+
__weak AVAsset* weakAsset = asset;
|
|
316
|
+
void (^trackCompletionHandler)(void) = ^{
|
|
317
|
+
RNSVVideoPlayer* owner = weakSelf;
|
|
318
|
+
if (!owner) return;
|
|
319
|
+
owner->_tracksLoading = false;
|
|
320
|
+
AVAsset* loadedAsset = weakAsset;
|
|
321
|
+
if (owner->_disposed || !loadedAsset || [loadedAsset statusOfValueForKey:@"tracks"
|
|
322
|
+
error:nil] != AVKeyValueStatusLoaded) {
|
|
323
|
+
// Cancelled, or something failed.
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
327
|
+
[weakSelf dispatchReadyToPlay];
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
[asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ]
|
|
331
|
+
completionHandler:trackCompletionHandler];
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
NSArray* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
|
|
336
|
+
NSArray* allTracks = asset.tracks;
|
|
337
|
+
BOOL hasVideoTracks = videoTracks.count != 0;
|
|
338
|
+
BOOL hasNoTracks = allTracks.count == 0;
|
|
339
|
+
|
|
340
|
+
// The player has not yet initialized when it has no size, unless it is an
|
|
341
|
+
// audio-only track. HLS m3u8 video files never load any tracks, and are
|
|
342
|
+
// also not yet initialized until they have a size.
|
|
343
|
+
if ((hasVideoTracks || hasNoTracks) && height == CGSizeZero.height &&
|
|
344
|
+
width == CGSizeZero.width) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
AVAssetTrack* track = [videoTracks firstObject];
|
|
348
|
+
int rotation =
|
|
349
|
+
RNSkiaVideo::AVAssetTrackUtils::GetTrackRotationInDegree(track);
|
|
350
|
+
|
|
351
|
+
// The player may be initialized but still needs to determine the duration.
|
|
352
|
+
CMTime duration = currentItem.duration;
|
|
353
|
+
if (CMTIME_IS_INDEFINITE(duration) || duration.timescale == 0 ||
|
|
354
|
+
duration.value == 0) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
_isInitialized = YES;
|
|
359
|
+
[_delegate readyToPlay:@{
|
|
360
|
+
@"width" : @(width),
|
|
361
|
+
@"height" : @(height),
|
|
362
|
+
@"rotation" : @(rotation)
|
|
363
|
+
}];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
- (void)updatePlayingState {
|
|
368
|
+
if (!_isInitialized) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (_isPlaying && _player.rate == 0) {
|
|
372
|
+
_player.rate = _playbackSpeed;
|
|
373
|
+
} else if (!_isPlaying && _player.rate != 0) {
|
|
374
|
+
[_player pause];
|
|
375
|
+
}
|
|
376
|
+
// If the texture is still waiting for an expected frame, the display link
|
|
377
|
+
// needs to keep running until it arrives regardless of the play/pause state.
|
|
378
|
+
_displayLink.paused = !_isPlaying && !_waitingForFrame;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
- (void)expectFrame {
|
|
382
|
+
_waitingForFrame = YES;
|
|
383
|
+
_displayLink.paused = NO;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
- (void)removeKeyValueObservers {
|
|
387
|
+
AVPlayerItem* currentItem = _player.currentItem;
|
|
388
|
+
[currentItem removeObserver:self forKeyPath:@"status"];
|
|
389
|
+
[currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
|
|
390
|
+
[currentItem removeObserver:self forKeyPath:@"presentationSize"];
|
|
391
|
+
[currentItem removeObserver:self forKeyPath:@"duration"];
|
|
392
|
+
[currentItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
|
|
393
|
+
[_player removeObserver:self forKeyPath:@"rate"];
|
|
394
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
- (void)dispose {
|
|
398
|
+
if (_disposed.exchange(true)) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
[_player.currentItem cancelPendingSeeks];
|
|
402
|
+
[_player.currentItem.asset cancelLoading];
|
|
403
|
+
[self removeKeyValueObservers];
|
|
404
|
+
[_displayLink invalidate];
|
|
405
|
+
[_player replaceCurrentItemWithPlayerItem:nil];
|
|
406
|
+
_player = nil;
|
|
407
|
+
_displayLink = nil;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
@end
|