Skip to content

Commit 04ddb02

Browse files
authored
fix(📸): Add audio support for videos (#2462)
1 parent bcab375 commit 04ddb02

File tree

16 files changed

+366
-373
lines changed

16 files changed

+366
-373
lines changed

‎docs/docs/video.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ React Native Skia provides a way to load video frames as images, enabling rich m
99

1010
## Requirements
1111

12+
- **Reanimated** version 3 or higher.
1213
- **Android:** API level 26 or higher.
1314
- **Video URL:** Must be a local path. We recommend using it in combination with [expo-asset](https://docs.expo.dev/versions/latest/sdk/asset/) to download the video.
14-
- **Animated Playback:** Available only via [Reanimated 3](/docs/animations/animations) and above.
15-
- **Sound Playback:** Coming soon. In the meantime, audio can be played using [expo-av](https://docs.expo.dev/versions/latest/sdk/av/).
1615

1716
## Example
1817

‎example/ios/Podfile.lock

+6
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,8 @@ PODS:
356356
- React
357357
- React-callinvoker
358358
- React-Core
359+
- react-native-slider (4.4.2):
360+
- React-Core
359361
- React-perflogger (0.71.7)
360362
- React-RCTActionSheet (0.71.7):
361363
- React-Core/RCTActionSheetHeaders (= 0.71.7)
@@ -511,6 +513,7 @@ DEPENDENCIES:
511513
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
512514
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
513515
- "react-native-skia (from `../node_modules/@shopify/react-native-skia`)"
516+
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
514517
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
515518
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
516519
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
@@ -607,6 +610,8 @@ EXTERNAL SOURCES:
607610
:path: "../node_modules/react-native-safe-area-context"
608611
react-native-skia:
609612
:path: "../node_modules/@shopify/react-native-skia"
613+
react-native-slider:
614+
:path: "../node_modules/@react-native-community/slider"
610615
React-perflogger:
611616
:path: "../node_modules/react-native/ReactCommon/reactperflogger"
612617
React-RCTActionSheet:
@@ -687,6 +692,7 @@ SPEC CHECKSUMS:
687692
React-logger: 3f8ebad1be1bf3299d1ab6d7f971802d7395c7ef
688693
react-native-safe-area-context: dfe5aa13bee37a0c7e8059d14f72ffc076d120e9
689694
react-native-skia: c2c416b864962e73d8b9c81f0fa399ee89c8435e
695+
react-native-slider: 33b8d190b59d4f67a541061bb91775d53d617d9d
690696
React-perflogger: 2d505bbe298e3b7bacdd9e542b15535be07220f6
691697
React-RCTActionSheet: 0e96e4560bd733c9b37efbf68f5b1a47615892fb
692698
React-RCTAnimation: fd138e26f120371c87e406745a27535e2c8a04ef

‎example/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"android-reverse-tcp": "adb devices | grep '\t' | awk '{print $1}' | sed 's/\\s//g' | xargs -I {} adb -s {} reverse tcp:8081 tcp:8081"
1515
},
1616
"dependencies": {
17+
"@react-native-community/slider": "4.4.2",
1718
"@react-navigation/bottom-tabs": "6.5.7",
1819
"@react-navigation/elements": "1.3.6",
1920
"@react-navigation/native": "6.0.13",

‎example/src/Examples/Video/Video.tsx

+58-25
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,78 @@ import {
44
ColorMatrix,
55
Fill,
66
ImageShader,
7+
Text,
8+
useFont,
79
} from "@shopify/react-native-skia";
8-
import { Pressable, useWindowDimensions } from "react-native";
9-
import { useSharedValue } from "react-native-reanimated";
10+
import { Pressable, View, useWindowDimensions } from "react-native";
11+
import { useDerivedValue, useSharedValue } from "react-native-reanimated";
12+
import Slider from "@react-native-community/slider";
1013

1114
import { useVideoFromAsset } from "../../components/Animations";
1215

1316
export const Video = () => {
1417
const paused = useSharedValue(false);
18+
const seek = useSharedValue(0);
1519
const { width, height } = useWindowDimensions();
16-
const { currentFrame } = useVideoFromAsset(
20+
const fontSize = 20;
21+
const font = useFont(require("../../assets/SF-Mono-Semibold.otf"), fontSize);
22+
const { currentFrame, currentTime, duration } = useVideoFromAsset(
1723
require("../../Tests/assets/BigBuckBunny.mp4"),
1824
{
1925
paused,
2026
looping: true,
27+
seek,
28+
volume: 0,
2129
}
2230
);
31+
const text = useDerivedValue(() => currentTime.value.toFixed(0));
2332
return (
24-
<Pressable
25-
style={{ flex: 1 }}
26-
onPress={() => (paused.value = !paused.value)}
27-
>
28-
<Canvas style={{ flex: 1 }}>
29-
<Fill>
30-
<ImageShader
31-
image={currentFrame}
32-
x={0}
33-
y={0}
34-
width={width}
35-
height={height}
36-
fit="cover"
33+
<View style={{ flex: 1 }}>
34+
<Pressable
35+
style={{ flex: 1 }}
36+
onPress={() => (paused.value = !paused.value)}
37+
>
38+
<Canvas style={{ flex: 1 }}>
39+
<Fill>
40+
<ImageShader
41+
image={currentFrame}
42+
x={0}
43+
y={0}
44+
width={width}
45+
height={height}
46+
fit="cover"
47+
/>
48+
<ColorMatrix
49+
matrix={[
50+
0.95, 0, 0, 0, 0.05, 0.65, 0, 0, 0, 0.15, 0.15, 0, 0, 0, 0.5, 0,
51+
0, 0, 1, 0,
52+
]}
53+
/>
54+
</Fill>
55+
<Text
56+
x={20}
57+
y={height - 200 - 2 * fontSize}
58+
text={text}
59+
font={font}
3760
/>
38-
<ColorMatrix
39-
matrix={[
40-
0.95, 0, 0, 0, 0.05, 0.65, 0, 0, 0, 0.15, 0.15, 0, 0, 0, 0.5, 0,
41-
0, 0, 1, 0,
42-
]}
43-
/>
44-
</Fill>
45-
</Canvas>
46-
</Pressable>
61+
</Canvas>
62+
</Pressable>
63+
<View style={{ height: 200 }}>
64+
<Slider
65+
style={{ width, height: 40 }}
66+
minimumValue={0}
67+
maximumValue={1}
68+
minimumTrackTintColor="#FFFFFF"
69+
maximumTrackTintColor="#000000"
70+
onSlidingComplete={(value) => {
71+
seek.value = value * duration;
72+
paused.value = false;
73+
}}
74+
onSlidingStart={() => {
75+
paused.value = true;
76+
}}
77+
/>
78+
</View>
79+
</View>
4780
);
4881
};

‎example/yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -2379,6 +2379,11 @@
23792379
resolved "https://registry.yarnpkg.com/@react-native-community/eslint-plugin/-/eslint-plugin-1.3.0.tgz#9e558170c106bbafaa1ef502bd8e6d4651012bf9"
23802380
integrity sha512-+zDZ20NUnSWghj7Ku5aFphMzuM9JulqCW+aPXT6IfIXFbb8tzYTTOSeRFOtuekJ99ibW2fUCSsjuKNlwDIbHFg==
23812381

2382+
"@react-native-community/[email protected]":
2383+
version "4.4.2"
2384+
resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-4.4.2.tgz#1fea0eb3ae31841fe87bd6c4fc67569066e9cf4b"
2385+
integrity sha512-D9bv+3Vd2gairAhnRPAghwccgEmoM7g562pm8i4qB3Esrms5mggF81G3UvCyc0w3jjtFHh8dpQkfEoKiP0NW/Q==
2386+
23822387
"@react-native/[email protected]":
23832388
version "1.0.0"
23842389
resolved "https://registry.yarnpkg.com/@react-native/assets/-/assets-1.0.0.tgz#c6f9bf63d274bafc8e970628de24986b30a55c8e"

‎package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp

+33-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ double RNSkAndroidVideo::framerate() {
8383
void RNSkAndroidVideo::seek(double timestamp) {
8484
JNIEnv *env = facebook::jni::Environment::current();
8585
jclass cls = env->GetObjectClass(_jniVideo.get());
86-
jmethodID mid = env->GetMethodID(cls, "seek", "(J)V");
86+
jmethodID mid = env->GetMethodID(cls, "seek", "(D)V");
8787
if (!mid) {
8888
RNSkLogger::logToConsole("seek method not found");
8989
return;
@@ -128,4 +128,36 @@ SkISize RNSkAndroidVideo::getSize() {
128128
return SkISize::Make(width, height);
129129
}
130130

131+
void RNSkAndroidVideo::play() {
132+
JNIEnv *env = facebook::jni::Environment::current();
133+
jclass cls = env->GetObjectClass(_jniVideo.get());
134+
jmethodID mid = env->GetMethodID(cls, "play", "()V");
135+
if (!mid) {
136+
RNSkLogger::logToConsole("play method not found");
137+
return;
138+
}
139+
env->CallVoidMethod(_jniVideo.get(), mid);
140+
}
141+
142+
void RNSkAndroidVideo::pause() {
143+
JNIEnv *env = facebook::jni::Environment::current();
144+
jclass cls = env->GetObjectClass(_jniVideo.get());
145+
jmethodID mid = env->GetMethodID(cls, "pause", "()V");
146+
if (!mid) {
147+
RNSkLogger::logToConsole("pause method not found");
148+
return;
149+
}
150+
env->CallVoidMethod(_jniVideo.get(), mid);
151+
}
152+
153+
void RNSkAndroidVideo::setVolume(float volume) {
154+
JNIEnv *env = facebook::jni::Environment::current();
155+
jclass cls = env->GetObjectClass(_jniVideo.get());
156+
jmethodID mid = env->GetMethodID(cls, "setVolume", "(F)V");
157+
if (!mid) {
158+
RNSkLogger::logToConsole("setVolume method not found");
159+
return;
160+
}
161+
env->CallVoidMethod(_jniVideo.get(), mid, volume);
162+
}
131163
} // namespace RNSkia

‎package/android/cpp/rnskia-android/RNSkAndroidVideo.h

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ class RNSkAndroidVideo : public RNSkVideo {
3333
void seek(double timestamp) override;
3434
float getRotationInDegrees() override;
3535
SkISize getSize() override;
36+
void play() override;
37+
void pause() override;
38+
void setVolume(float volume) override;
3639
};
3740

3841
} // namespace RNSkia

‎package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java

+70-5
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
import android.content.Context;
44
import android.graphics.ImageFormat;
55
import android.hardware.HardwareBuffer;
6-
import android.media.Image;
7-
import android.media.ImageReader;
6+
import android.media.AudioAttributes;
7+
import android.media.AudioManager;
88
import android.media.MediaCodec;
99
import android.media.MediaExtractor;
1010
import android.media.MediaFormat;
11+
import android.media.MediaPlayer;
12+
import android.media.MediaSync;
13+
import android.media.Image;
14+
import android.media.ImageReader;
1115
import android.net.Uri;
1216
import android.os.Build;
1317
import android.view.Surface;
@@ -28,12 +32,16 @@ public class RNSkVideo {
2832
private MediaCodec decoder;
2933
private ImageReader imageReader;
3034
private Surface outputSurface;
35+
private MediaPlayer mediaPlayer;
36+
private MediaSync mediaSync;
3137
private double durationMs;
3238
private double frameRate;
3339
private int rotationDegrees = 0;
3440
private int width = 0;
3541
private int height = 0;
3642

43+
private boolean isPlaying = false;
44+
3745
RNSkVideo(Context context, String localUri) {
3846
this.uri = Uri.parse(localUri);
3947
this.context = context;
@@ -50,6 +58,18 @@ private void initializeReader() {
5058
}
5159
extractor.selectTrack(trackIndex);
5260
MediaFormat format = extractor.getTrackFormat(trackIndex);
61+
62+
// Initialize MediaPlayer
63+
mediaPlayer = new MediaPlayer();
64+
mediaPlayer.setDataSource(context, uri);
65+
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
66+
mediaPlayer.setOnPreparedListener(mp -> {
67+
durationMs = mp.getDuration();
68+
mp.start();
69+
isPlaying = true;
70+
});
71+
mediaPlayer.prepareAsync();
72+
5373
// Retrieve and store video properties
5474
if (format.containsKey(MediaFormat.KEY_DURATION)) {
5575
durationMs = format.getLong(MediaFormat.KEY_DURATION) / 1000; // Convert microseconds to milliseconds
@@ -119,12 +139,30 @@ public HardwareBuffer nextImage() {
119139
}
120140

121141
@DoNotStrip
122-
public void seek(long timestamp) {
123-
// Seek to the closest sync frame at or before the specified time
124-
extractor.seekTo(timestamp * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
142+
public void seek(double timestamp) {
143+
// Log the values for debugging
144+
145+
long timestampUs = (long)(timestamp * 1000); // Convert milliseconds to microseconds
146+
147+
extractor.seekTo(timestampUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
148+
if (mediaPlayer != null) {
149+
int timestampMs = (int) timestamp; // Convert to milliseconds
150+
mediaPlayer.seekTo(timestampMs, MediaPlayer.SEEK_CLOSEST);
151+
}
152+
125153
// Flush the codec to reset internal state and buffers
126154
if (decoder != null) {
127155
decoder.flush();
156+
157+
// Decode frames until reaching the exact timestamp
158+
boolean isSeeking = true;
159+
while (isSeeking) {
160+
decodeFrame();
161+
long currentTimestampUs = extractor.getSampleTime();
162+
if (currentTimestampUs >= timestampUs) {
163+
isSeeking = false;
164+
}
165+
}
128166
}
129167
}
130168

@@ -187,7 +225,34 @@ private void decodeFrame() {
187225
}
188226
}
189227

228+
@DoNotStrip
229+
public void play() {
230+
if (mediaPlayer != null && !isPlaying) {
231+
mediaPlayer.start();
232+
isPlaying = true;
233+
}
234+
}
235+
236+
@DoNotStrip
237+
public void pause() {
238+
if (mediaPlayer != null && isPlaying) {
239+
mediaPlayer.pause();
240+
isPlaying = false;
241+
}
242+
}
243+
244+
@DoNotStrip
245+
public void setVolume(float volume) {
246+
if (mediaPlayer != null) {
247+
mediaPlayer.setVolume(volume, volume);
248+
}
249+
}
250+
190251
public void release() {
252+
if (mediaPlayer != null) {
253+
mediaPlayer.release();
254+
mediaPlayer = null;
255+
}
191256
if (decoder != null) {
192257
decoder.stop();
193258
decoder.release();

‎package/cpp/api/JsiVideo.h

+22-7
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,28 @@ class JsiVideo : public JsiSkWrappingSharedPtrHostObject<RNSkVideo> {
6868
return result;
6969
}
7070

71-
JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiVideo, nextImage),
72-
JSI_EXPORT_FUNC(JsiVideo, duration),
73-
JSI_EXPORT_FUNC(JsiVideo, framerate),
74-
JSI_EXPORT_FUNC(JsiVideo, seek),
75-
JSI_EXPORT_FUNC(JsiVideo, rotation),
76-
JSI_EXPORT_FUNC(JsiVideo, size),
77-
JSI_EXPORT_FUNC(JsiVideo, dispose))
71+
JSI_HOST_FUNCTION(play) {
72+
getObject()->play();
73+
return jsi::Value::undefined();
74+
}
75+
76+
JSI_HOST_FUNCTION(pause) {
77+
getObject()->pause();
78+
return jsi::Value::undefined();
79+
}
80+
81+
JSI_HOST_FUNCTION(setVolume) {
82+
auto volume = arguments[0].asNumber();
83+
getObject()->setVolume(static_cast<float>(volume));
84+
return jsi::Value::undefined();
85+
}
86+
87+
JSI_EXPORT_FUNCTIONS(
88+
JSI_EXPORT_FUNC(JsiVideo, nextImage), JSI_EXPORT_FUNC(JsiVideo, duration),
89+
JSI_EXPORT_FUNC(JsiVideo, framerate), JSI_EXPORT_FUNC(JsiVideo, seek),
90+
JSI_EXPORT_FUNC(JsiVideo, rotation), JSI_EXPORT_FUNC(JsiVideo, size),
91+
JSI_EXPORT_FUNC(JsiVideo, play), JSI_EXPORT_FUNC(JsiVideo, pause),
92+
JSI_EXPORT_FUNC(JsiVideo, setVolume), JSI_EXPORT_FUNC(JsiVideo, dispose))
7893

7994
JsiVideo(std::shared_ptr<RNSkPlatformContext> context,
8095
std::shared_ptr<RNSkVideo> video)

‎package/cpp/rnskia/RNSkVideo.h

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ class RNSkVideo {
2020
virtual void seek(double timestamp) = 0;
2121
virtual float getRotationInDegrees() = 0;
2222
virtual SkISize getSize() = 0;
23+
virtual void play() = 0;
24+
virtual void pause() = 0;
25+
virtual void setVolume(float volume) = 0;
2326
};
2427

2528
} // namespace RNSkia

0 commit comments

Comments
 (0)