react-native-skia-webgpu-video 0.0.0-stage → 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/DisplayVsyncPeriod.java +72 -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 +43 -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 +114 -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 +779 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +463 -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 +368 -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 +105 -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 +155 -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 +568 -0
- package/src/videoFrameImage.ts +205 -0
- package/src/videoPlayer.ts +318 -0
- package/src/yuvBlit.ts +331 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
package com.azzapp.rnskv;
|
|
2
|
+
|
|
3
|
+
import android.media.MediaCodec;
|
|
4
|
+
import android.media.MediaCodecInfo;
|
|
5
|
+
import android.media.MediaExtractor;
|
|
6
|
+
import android.media.MediaFormat;
|
|
7
|
+
|
|
8
|
+
import java.nio.ByteBuffer;
|
|
9
|
+
import java.nio.ByteOrder;
|
|
10
|
+
import java.nio.ShortBuffer;
|
|
11
|
+
import java.util.ArrayList;
|
|
12
|
+
import java.util.Arrays;
|
|
13
|
+
import java.util.List;
|
|
14
|
+
import java.util.function.BooleanSupplier;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Decodes, mixes and encodes (AAC) the audio of the audio-enabled items of a
|
|
18
|
+
* video composition. The pipeline is synchronous and meant to be run on a
|
|
19
|
+
* dedicated thread, in parallel of the video frames encoding.
|
|
20
|
+
*/
|
|
21
|
+
public class AudioCompositionExporter {
|
|
22
|
+
|
|
23
|
+
public static final String AUDIO_MIME_TYPE = "audio/mp4a-latm";
|
|
24
|
+
|
|
25
|
+
private static final int CHUNK_FRAMES = 2048;
|
|
26
|
+
|
|
27
|
+
private static final long CODEC_TIMEOUT_US = 10000;
|
|
28
|
+
|
|
29
|
+
private static final int MAX_STALLED_ITERATIONS = 1000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Receives the encoded AAC samples, on the thread running {@link #run}.
|
|
33
|
+
*/
|
|
34
|
+
public interface Sink {
|
|
35
|
+
void onAudioFormat(MediaFormat format);
|
|
36
|
+
|
|
37
|
+
void onAudioSample(ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private final VideoComposition composition;
|
|
41
|
+
|
|
42
|
+
private final int sampleRate;
|
|
43
|
+
|
|
44
|
+
private final int channelCount;
|
|
45
|
+
|
|
46
|
+
private final int bitRate;
|
|
47
|
+
|
|
48
|
+
private final Sink sink;
|
|
49
|
+
|
|
50
|
+
private final BooleanSupplier isCanceled;
|
|
51
|
+
|
|
52
|
+
private MediaCodec encoder;
|
|
53
|
+
|
|
54
|
+
private final MediaCodec.BufferInfo encoderBufferInfo = new MediaCodec.BufferInfo();
|
|
55
|
+
|
|
56
|
+
private boolean formatDispatched = false;
|
|
57
|
+
|
|
58
|
+
public AudioCompositionExporter(
|
|
59
|
+
VideoComposition composition,
|
|
60
|
+
int sampleRate,
|
|
61
|
+
int channelCount,
|
|
62
|
+
int bitRate,
|
|
63
|
+
Sink sink,
|
|
64
|
+
BooleanSupplier isCanceled
|
|
65
|
+
) {
|
|
66
|
+
this.composition = composition;
|
|
67
|
+
this.sampleRate = sampleRate;
|
|
68
|
+
this.channelCount = channelCount;
|
|
69
|
+
this.bitRate = bitRate;
|
|
70
|
+
this.sink = sink;
|
|
71
|
+
this.isCanceled = isCanceled;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Runs the whole audio export pipeline, returns once every audio sample
|
|
76
|
+
* of the composition has been encoded and handed to the sink.
|
|
77
|
+
*/
|
|
78
|
+
public void run() throws Exception {
|
|
79
|
+
List<ItemSlot> slots = new ArrayList<>();
|
|
80
|
+
try {
|
|
81
|
+
for (VideoComposition.Item item : composition.getItems()) {
|
|
82
|
+
if (!item.isAudioEnabled()) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
slots.add(new ItemSlot(item));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
encoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE);
|
|
89
|
+
MediaFormat format =
|
|
90
|
+
MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, sampleRate, channelCount);
|
|
91
|
+
format.setInteger(
|
|
92
|
+
MediaFormat.KEY_AAC_PROFILE,
|
|
93
|
+
MediaCodecInfo.CodecProfileLevel.AACObjectLC
|
|
94
|
+
);
|
|
95
|
+
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
|
96
|
+
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
|
97
|
+
encoder.start();
|
|
98
|
+
|
|
99
|
+
long totalFrames = Math.round(composition.getDuration() * sampleRate);
|
|
100
|
+
int[] mixBuffer = new int[CHUNK_FRAMES * channelCount];
|
|
101
|
+
long framePosition = 0;
|
|
102
|
+
while (framePosition < totalFrames) {
|
|
103
|
+
if (isCanceled.getAsBoolean()) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
int chunkFrames = (int) Math.min(CHUNK_FRAMES, totalFrames - framePosition);
|
|
107
|
+
long chunkEndFrame = framePosition + chunkFrames;
|
|
108
|
+
Arrays.fill(mixBuffer, 0);
|
|
109
|
+
for (ItemSlot slot : slots) {
|
|
110
|
+
if (slot.done || chunkEndFrame <= slot.startFrame) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// Decoders are created when the timeline enters the item and
|
|
114
|
+
// released when it leaves it, so the number of simultaneous
|
|
115
|
+
// codec instances stays bounded by the overlapping items.
|
|
116
|
+
if (slot.decoder == null) {
|
|
117
|
+
slot.decoder = new ItemDecoder(slot.item);
|
|
118
|
+
}
|
|
119
|
+
slot.decoder.mixInto(mixBuffer, framePosition, chunkFrames);
|
|
120
|
+
if (chunkEndFrame >= slot.endFrame) {
|
|
121
|
+
slot.decoder.release();
|
|
122
|
+
slot.decoder = null;
|
|
123
|
+
slot.done = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
queuePcm(mixBuffer, chunkFrames, framePosition);
|
|
127
|
+
framePosition += chunkFrames;
|
|
128
|
+
}
|
|
129
|
+
queueEndOfStream();
|
|
130
|
+
} finally {
|
|
131
|
+
for (ItemSlot slot : slots) {
|
|
132
|
+
if (slot.decoder != null) {
|
|
133
|
+
slot.decoder.release();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (encoder != null) {
|
|
137
|
+
try {
|
|
138
|
+
encoder.stop();
|
|
139
|
+
} catch (IllegalStateException ignored) {
|
|
140
|
+
}
|
|
141
|
+
encoder.release();
|
|
142
|
+
encoder = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private void queuePcm(int[] samples, int frames, long framePosition) {
|
|
148
|
+
int bytesPerFrame = channelCount * 2;
|
|
149
|
+
int totalBytes = frames * bytesPerFrame;
|
|
150
|
+
int offsetBytes = 0;
|
|
151
|
+
int stalled = 0;
|
|
152
|
+
while (offsetBytes < totalBytes) {
|
|
153
|
+
if (isCanceled.getAsBoolean() || Thread.currentThread().isInterrupted()) return;
|
|
154
|
+
int inputIndex = encoder.dequeueInputBuffer(CODEC_TIMEOUT_US);
|
|
155
|
+
if (inputIndex < 0) {
|
|
156
|
+
drainEncoder(false);
|
|
157
|
+
if (++stalled > MAX_STALLED_ITERATIONS) {
|
|
158
|
+
throw new RuntimeException("Audio encoder stalled");
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
stalled = 0;
|
|
163
|
+
ByteBuffer input = encoder.getInputBuffer(inputIndex);
|
|
164
|
+
if (input == null) {
|
|
165
|
+
throw new RuntimeException("Audio encoder input buffer was null");
|
|
166
|
+
}
|
|
167
|
+
input.clear();
|
|
168
|
+
input.order(ByteOrder.LITTLE_ENDIAN);
|
|
169
|
+
int bytes = Math.min(input.remaining(), totalBytes - offsetBytes);
|
|
170
|
+
bytes -= bytes % bytesPerFrame;
|
|
171
|
+
ShortBuffer shortBuffer = input.asShortBuffer();
|
|
172
|
+
int sampleOffset = offsetBytes / 2;
|
|
173
|
+
int sampleCount = bytes / 2;
|
|
174
|
+
for (int i = 0; i < sampleCount; i++) {
|
|
175
|
+
int sample = samples[sampleOffset + i];
|
|
176
|
+
shortBuffer.put(
|
|
177
|
+
(short) Math.max(Short.MIN_VALUE, Math.min(Short.MAX_VALUE, sample)));
|
|
178
|
+
}
|
|
179
|
+
long ptsUs =
|
|
180
|
+
(framePosition + offsetBytes / bytesPerFrame) * 1000000L / sampleRate;
|
|
181
|
+
encoder.queueInputBuffer(inputIndex, 0, bytes, ptsUs, 0);
|
|
182
|
+
offsetBytes += bytes;
|
|
183
|
+
drainEncoder(false);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private void queueEndOfStream() {
|
|
188
|
+
int stalled = 0;
|
|
189
|
+
while (true) {
|
|
190
|
+
if (isCanceled.getAsBoolean() || Thread.currentThread().isInterrupted()) return;
|
|
191
|
+
int inputIndex = encoder.dequeueInputBuffer(CODEC_TIMEOUT_US);
|
|
192
|
+
if (inputIndex >= 0) {
|
|
193
|
+
encoder.queueInputBuffer(
|
|
194
|
+
inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
drainEncoder(false);
|
|
198
|
+
if (++stalled > MAX_STALLED_ITERATIONS) {
|
|
199
|
+
throw new RuntimeException("Audio encoder stalled");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
drainEncoder(true);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private void drainEncoder(boolean endOfStream) {
|
|
206
|
+
long deadlineNs = System.nanoTime() + 30_000_000_000L;
|
|
207
|
+
while (true) {
|
|
208
|
+
if (isCanceled.getAsBoolean() || Thread.currentThread().isInterrupted()) return;
|
|
209
|
+
if (System.nanoTime() > deadlineNs) throw new IllegalStateException("Audio encoder stalled");
|
|
210
|
+
int encoderStatus =
|
|
211
|
+
encoder.dequeueOutputBuffer(encoderBufferInfo, endOfStream ? CODEC_TIMEOUT_US : 0);
|
|
212
|
+
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
|
|
213
|
+
if (!endOfStream) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
217
|
+
if (formatDispatched) {
|
|
218
|
+
throw new RuntimeException("Audio format changed twice");
|
|
219
|
+
}
|
|
220
|
+
formatDispatched = true;
|
|
221
|
+
sink.onAudioFormat(encoder.getOutputFormat());
|
|
222
|
+
} else if (encoderStatus >= 0) {
|
|
223
|
+
ByteBuffer encodedData = encoder.getOutputBuffer(encoderStatus);
|
|
224
|
+
if (encodedData == null) {
|
|
225
|
+
throw new RuntimeException(
|
|
226
|
+
"audioEncoderOutputBuffer " + encoderStatus + " was null");
|
|
227
|
+
}
|
|
228
|
+
if ((encoderBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
|
229
|
+
encoderBufferInfo.size = 0;
|
|
230
|
+
}
|
|
231
|
+
if (encoderBufferInfo.size != 0) {
|
|
232
|
+
encodedData.position(encoderBufferInfo.offset);
|
|
233
|
+
encodedData.limit(encoderBufferInfo.offset + encoderBufferInfo.size);
|
|
234
|
+
sink.onAudioSample(encodedData, encoderBufferInfo);
|
|
235
|
+
}
|
|
236
|
+
encoder.releaseOutputBuffer(encoderStatus, false);
|
|
237
|
+
if ((encoderBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* An audio-enabled item of the composition and its decoder; the decoder
|
|
246
|
+
* only exists while the export timeline overlaps the item.
|
|
247
|
+
*/
|
|
248
|
+
private class ItemSlot {
|
|
249
|
+
final VideoComposition.Item item;
|
|
250
|
+
final long startFrame;
|
|
251
|
+
final long endFrame;
|
|
252
|
+
ItemDecoder decoder;
|
|
253
|
+
boolean done = false;
|
|
254
|
+
|
|
255
|
+
ItemSlot(VideoComposition.Item item) {
|
|
256
|
+
this.item = item;
|
|
257
|
+
startFrame = Math.round(item.getCompositionStartTime() * sampleRate);
|
|
258
|
+
endFrame = startFrame + Math.round(item.getDuration() * sampleRate);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Decodes the audio track of a composition item, resampled to the output
|
|
264
|
+
* sample rate/channel count, and mixes it into the composition timeline.
|
|
265
|
+
*/
|
|
266
|
+
private class ItemDecoder {
|
|
267
|
+
|
|
268
|
+
private final long itemStartFrame;
|
|
269
|
+
|
|
270
|
+
private final long itemEndFrame;
|
|
271
|
+
|
|
272
|
+
private final double volume;
|
|
273
|
+
|
|
274
|
+
private final long startTimeUs;
|
|
275
|
+
|
|
276
|
+
private final long endTimeUs;
|
|
277
|
+
|
|
278
|
+
private final MediaExtractor extractor;
|
|
279
|
+
|
|
280
|
+
private MediaCodec decoder;
|
|
281
|
+
|
|
282
|
+
private boolean hasAudioTrack = true;
|
|
283
|
+
|
|
284
|
+
private int srcSampleRate;
|
|
285
|
+
|
|
286
|
+
private int srcChannelCount;
|
|
287
|
+
|
|
288
|
+
private boolean inputDone = false;
|
|
289
|
+
|
|
290
|
+
private boolean outputDone = false;
|
|
291
|
+
|
|
292
|
+
private boolean firstBuffer = true;
|
|
293
|
+
|
|
294
|
+
private final MediaCodec.BufferInfo decoderBufferInfo = new MediaCodec.BufferInfo();
|
|
295
|
+
|
|
296
|
+
// Decoded source samples (interleaved), starting at bufferStartFrame
|
|
297
|
+
// (source frame index, 0 = item startTime).
|
|
298
|
+
private short[] buffer = new short[0];
|
|
299
|
+
|
|
300
|
+
private long bufferStartFrame = 0;
|
|
301
|
+
|
|
302
|
+
private int bufferFrames = 0;
|
|
303
|
+
|
|
304
|
+
ItemDecoder(VideoComposition.Item item) throws Exception {
|
|
305
|
+
itemStartFrame = Math.round(item.getCompositionStartTime() * sampleRate);
|
|
306
|
+
itemEndFrame = itemStartFrame + Math.round(item.getDuration() * sampleRate);
|
|
307
|
+
volume = Math.max(0, Math.min(1, item.getAudioVolume()));
|
|
308
|
+
startTimeUs = TimeHelpers.secToUs(item.getStartTime());
|
|
309
|
+
endTimeUs = startTimeUs + TimeHelpers.secToUs(item.getDuration());
|
|
310
|
+
|
|
311
|
+
extractor = new MediaExtractor();
|
|
312
|
+
try {
|
|
313
|
+
extractor.setDataSource(item.getPath());
|
|
314
|
+
int audioTrackIndex = -1;
|
|
315
|
+
MediaFormat audioFormat = null;
|
|
316
|
+
for (int i = 0; i < extractor.getTrackCount(); i++) {
|
|
317
|
+
MediaFormat trackFormat = extractor.getTrackFormat(i);
|
|
318
|
+
String mime = trackFormat.getString(MediaFormat.KEY_MIME);
|
|
319
|
+
if (mime != null && mime.startsWith("audio/")) {
|
|
320
|
+
audioTrackIndex = i;
|
|
321
|
+
audioFormat = trackFormat;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (audioTrackIndex == -1) {
|
|
326
|
+
if (item.isVideo()) {
|
|
327
|
+
// Video item without audio track: keep it silent.
|
|
328
|
+
hasAudioTrack = false;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
throw new RuntimeException(
|
|
332
|
+
"No audio track for path: " + item.getPath());
|
|
333
|
+
}
|
|
334
|
+
extractor.selectTrack(audioTrackIndex);
|
|
335
|
+
extractor.seekTo(startTimeUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
|
|
336
|
+
|
|
337
|
+
srcSampleRate = audioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
|
338
|
+
srcChannelCount = audioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
|
339
|
+
String mime = audioFormat.getString(MediaFormat.KEY_MIME);
|
|
340
|
+
decoder = MediaCodec.createDecoderByType(mime);
|
|
341
|
+
decoder.configure(audioFormat, null, null, 0);
|
|
342
|
+
decoder.start();
|
|
343
|
+
} catch (Exception e) {
|
|
344
|
+
release();
|
|
345
|
+
throw e;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
void mixInto(int[] mix, long chunkStartFrame, int frames) {
|
|
350
|
+
if (!hasAudioTrack) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
long overlapStart = Math.max(chunkStartFrame, itemStartFrame);
|
|
354
|
+
long overlapEnd = Math.min(chunkStartFrame + frames, itemEndFrame);
|
|
355
|
+
if (overlapEnd <= overlapStart) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
for (long frame = overlapStart; frame < overlapEnd; frame++) {
|
|
359
|
+
long localFrame = frame - itemStartFrame;
|
|
360
|
+
double srcFrame = localFrame * srcSampleRate / (double) sampleRate;
|
|
361
|
+
long srcFrameIndex = (long) srcFrame;
|
|
362
|
+
if (!ensureSamplesUpTo(srcFrameIndex + 2)) {
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
double fraction = srcFrame - srcFrameIndex;
|
|
366
|
+
int mixIndex = (int) (frame - chunkStartFrame) * channelCount;
|
|
367
|
+
for (int channel = 0; channel < channelCount; channel++) {
|
|
368
|
+
double sample = readSample(srcFrameIndex, channel) * (1 - fraction)
|
|
369
|
+
+ readSample(srcFrameIndex + 1, channel) * fraction;
|
|
370
|
+
// Clipping only happens once every track has been accumulated
|
|
371
|
+
// (in queuePcm), otherwise the result would depend on the order
|
|
372
|
+
// of the items.
|
|
373
|
+
mix[mixIndex + channel] += (int) Math.round(sample * volume);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Drop the consumed samples to keep memory bounded.
|
|
377
|
+
long consumedUpTo =
|
|
378
|
+
(long) ((overlapEnd - 1 - itemStartFrame) * srcSampleRate / (double) sampleRate);
|
|
379
|
+
dropSamplesBefore(consumedUpTo - 2);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private double readSample(long srcFrameIndex, int outChannel) {
|
|
383
|
+
long index = srcFrameIndex - bufferStartFrame;
|
|
384
|
+
if (index < 0 || index >= bufferFrames) {
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
int frameOffset = (int) index * srcChannelCount;
|
|
388
|
+
if (channelCount == 1 && srcChannelCount > 1) {
|
|
389
|
+
// Downmix to mono: average the source channels.
|
|
390
|
+
double sum = 0;
|
|
391
|
+
for (int channel = 0; channel < srcChannelCount; channel++) {
|
|
392
|
+
sum += buffer[frameOffset + channel];
|
|
393
|
+
}
|
|
394
|
+
return sum / srcChannelCount;
|
|
395
|
+
}
|
|
396
|
+
return buffer[frameOffset + Math.min(outChannel, srcChannelCount - 1)];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private boolean ensureSamplesUpTo(long srcFrameEnd) {
|
|
400
|
+
int stalled = 0;
|
|
401
|
+
while (bufferStartFrame + bufferFrames < srcFrameEnd) {
|
|
402
|
+
if (isCanceled.getAsBoolean() || Thread.currentThread().isInterrupted()) return false;
|
|
403
|
+
if (outputDone) {
|
|
404
|
+
// Past the end of the source: the remaining frames stay silent.
|
|
405
|
+
return bufferStartFrame + bufferFrames > 0
|
|
406
|
+
&& srcFrameEnd - (bufferStartFrame + bufferFrames) < 2;
|
|
407
|
+
}
|
|
408
|
+
if (!pumpDecoder()) {
|
|
409
|
+
if (++stalled > MAX_STALLED_ITERATIONS) {
|
|
410
|
+
throw new RuntimeException("Audio decoder stalled");
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
stalled = 0;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private boolean pumpDecoder() {
|
|
420
|
+
boolean progressed = false;
|
|
421
|
+
// Feed every input buffer the decoder offers before waiting on its
|
|
422
|
+
// output: a decoder needs several packets before it produces anything,
|
|
423
|
+
// and each output wait below can last CODEC_TIMEOUT_US.
|
|
424
|
+
while (!inputDone) {
|
|
425
|
+
if (isCanceled.getAsBoolean() || Thread.currentThread().isInterrupted()) return false;
|
|
426
|
+
int inputIndex = decoder.dequeueInputBuffer(0);
|
|
427
|
+
if (inputIndex < 0) {
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
ByteBuffer input = decoder.getInputBuffer(inputIndex);
|
|
431
|
+
int size = input != null ? extractor.readSampleData(input, 0) : -1;
|
|
432
|
+
long sampleTimeUs = extractor.getSampleTime();
|
|
433
|
+
if (size < 0 || sampleTimeUs >= endTimeUs) {
|
|
434
|
+
decoder.queueInputBuffer(
|
|
435
|
+
inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
|
436
|
+
inputDone = true;
|
|
437
|
+
} else {
|
|
438
|
+
decoder.queueInputBuffer(inputIndex, 0, size, sampleTimeUs, 0);
|
|
439
|
+
extractor.advance();
|
|
440
|
+
}
|
|
441
|
+
progressed = true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Only wait for output when no input could be fed: otherwise the decoder
|
|
445
|
+
// has work queued and the caller loops back here right away.
|
|
446
|
+
int outputIndex = decoder.dequeueOutputBuffer(
|
|
447
|
+
decoderBufferInfo, progressed ? 0 : CODEC_TIMEOUT_US);
|
|
448
|
+
if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
449
|
+
MediaFormat outputFormat = decoder.getOutputFormat();
|
|
450
|
+
srcSampleRate = outputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
|
451
|
+
srcChannelCount = outputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
if (outputIndex < 0) {
|
|
455
|
+
return progressed;
|
|
456
|
+
}
|
|
457
|
+
if ((decoderBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
458
|
+
outputDone = true;
|
|
459
|
+
}
|
|
460
|
+
ByteBuffer output = decoder.getOutputBuffer(outputIndex);
|
|
461
|
+
if (output != null && decoderBufferInfo.size > 0) {
|
|
462
|
+
output.position(decoderBufferInfo.offset);
|
|
463
|
+
output.limit(decoderBufferInfo.offset + decoderBufferInfo.size);
|
|
464
|
+
output.order(ByteOrder.LITTLE_ENDIAN);
|
|
465
|
+
int sampleCount = decoderBufferInfo.size / 2;
|
|
466
|
+
int frames = sampleCount / srcChannelCount;
|
|
467
|
+
int skipFrames = 0;
|
|
468
|
+
if (firstBuffer) {
|
|
469
|
+
firstBuffer = false;
|
|
470
|
+
long offsetUs = decoderBufferInfo.presentationTimeUs - startTimeUs;
|
|
471
|
+
if (offsetUs < 0) {
|
|
472
|
+
// Drop the samples decoded before the item start time (the
|
|
473
|
+
// extractor sought to a preceding sync point).
|
|
474
|
+
skipFrames = (int) Math.min(
|
|
475
|
+
Math.round(-offsetUs * srcSampleRate / 1000000.0),
|
|
476
|
+
frames
|
|
477
|
+
);
|
|
478
|
+
} else if (offsetUs > 0) {
|
|
479
|
+
// The audio starts after the requested start time: keep the
|
|
480
|
+
// offset so the audio stays aligned on the composition timeline.
|
|
481
|
+
bufferStartFrame = Math.round(offsetUs * srcSampleRate / 1000000.0);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
int keptFrames = frames - skipFrames;
|
|
485
|
+
if (keptFrames > 0) {
|
|
486
|
+
appendSamples(output, skipFrames * srcChannelCount, keptFrames);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
decoder.releaseOutputBuffer(outputIndex, false);
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private void appendSamples(ByteBuffer output, int skipSamples, int frames) {
|
|
494
|
+
int requiredFrames = bufferFrames + frames;
|
|
495
|
+
int requiredLength = requiredFrames * srcChannelCount;
|
|
496
|
+
if (buffer.length < requiredLength) {
|
|
497
|
+
buffer = Arrays.copyOf(buffer, Math.max(requiredLength, buffer.length * 2));
|
|
498
|
+
}
|
|
499
|
+
output.position(output.position() + skipSamples * 2);
|
|
500
|
+
output.asShortBuffer().get(
|
|
501
|
+
buffer, bufferFrames * srcChannelCount, frames * srcChannelCount);
|
|
502
|
+
bufferFrames = requiredFrames;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private void dropSamplesBefore(long srcFrame) {
|
|
506
|
+
long dropFrames = srcFrame - bufferStartFrame;
|
|
507
|
+
if (dropFrames <= 0) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
dropFrames = Math.min(dropFrames, bufferFrames);
|
|
511
|
+
int remainingFrames = bufferFrames - (int) dropFrames;
|
|
512
|
+
if (remainingFrames > 0) {
|
|
513
|
+
System.arraycopy(
|
|
514
|
+
buffer,
|
|
515
|
+
(int) dropFrames * srcChannelCount,
|
|
516
|
+
buffer,
|
|
517
|
+
0,
|
|
518
|
+
remainingFrames * srcChannelCount
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
bufferStartFrame += dropFrames;
|
|
522
|
+
bufferFrames = remainingFrames;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
void release() {
|
|
526
|
+
if (decoder != null) {
|
|
527
|
+
try {
|
|
528
|
+
decoder.stop();
|
|
529
|
+
} catch (IllegalStateException ignored) {
|
|
530
|
+
}
|
|
531
|
+
decoder.release();
|
|
532
|
+
decoder = null;
|
|
533
|
+
}
|
|
534
|
+
extractor.release();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|