Skip to content

Commit 9db45d7

Browse files
authored
feat: add playback speed as a configurable parameter in video player (#28)
* feat: add playback speed as a configurable parameter in video player * fix: update text color and reset speed button title in VideoPlayerExample
1 parent cf1aafe commit 9db45d7

File tree

10 files changed

+114
-5
lines changed

10 files changed

+114
-5
lines changed

android/cpp/VideoPlayer.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ void VideoPlayer::setIsLooping(jboolean isLooping) {
7070
setIsLoopingMethod(self(), isLooping);
7171
}
7272

73+
jfloat VideoPlayer::getPlaybackSpeed() {
74+
static const auto getPlaybackSpeedMethod =
75+
getClass()->getMethod<jfloat()>("getPlaybackSpeed");
76+
return getPlaybackSpeedMethod(self());
77+
}
78+
79+
void VideoPlayer::setPlaybackSpeed(jfloat playbackSpeed) {
80+
static const auto setPlaybackSpeedMethod =
81+
getClass()->getMethod<void(jfloat)>("setPlaybackSpeed");
82+
setPlaybackSpeedMethod(self(), playbackSpeed);
83+
}
84+
7385
local_ref<VideoFrame> VideoPlayer::decodeNextFrame() {
7486
static const auto decodeNextFrameMethod =
7587
getClass()->getMethod<VideoFrame()>("decodeNextFrame");

android/cpp/VideoPlayer.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ struct VideoPlayer : public jni::JavaClass<VideoPlayer> {
4141

4242
void setIsLooping(jboolean isLooping);
4343

44+
jfloat getPlaybackSpeed();
45+
46+
void setPlaybackSpeed(jfloat playbackSpeed);
47+
4448
local_ref<VideoFrame> decodeNextFrame();
4549

4650
void release();

android/cpp/VideoPlayerHostObject.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ VideoPlayerHostObject::getPropertyNames(jsi::Runtime& rt) {
2626
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("currentTime")));
2727
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("duration")));
2828
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("volume")));
29+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("playbackSpeed")));
2930
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isLooping")));
3031
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isPlaying")));
3132
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("dispose")));
@@ -97,6 +98,8 @@ jsi::Value VideoPlayerHostObject::get(jsi::Runtime& runtime,
9798
: (double)player->getDuration() / 1000.0);
9899
} else if (propName == "volume") {
99100
return jsi::Value(released.test() ? 0 : (double)player->getVolume());
101+
} else if (propName == "playbackSpeed") {
102+
return jsi::Value(released.test() ? 1 : (double)player->getPlaybackSpeed());
100103
} else if (propName == "isLooping") {
101104
return jsi::Value(!(released.test()) && player->getIsLooping());
102105
} else if (propName == "isPlaying") {
@@ -140,6 +143,8 @@ void VideoPlayerHostObject::set(facebook::jsi::Runtime& runtime,
140143
}
141144
if (propName == "volume") {
142145
player->setVolume(value.asNumber());
146+
} else if (propName == "playbackSpeed") {
147+
player->setPlaybackSpeed(value.asNumber());
143148
} else if (propName == "isLooping") {
144149
player->setIsLooping(value.asBool());
145150
}

android/src/main/java/com/azzapp/rnskv/VideoPlayer.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import androidx.media3.common.PlaybackException;
1010
import androidx.media3.common.Player;
1111
import androidx.media3.common.VideoSize;
12+
import androidx.media3.common.PlaybackParameters;
1213
import androidx.media3.common.util.UnstableApi;
1314
import androidx.media3.exoplayer.ExoPlayer;
1415

@@ -28,6 +29,8 @@ public class VideoPlayer {
2829

2930
private float volume = 1f;
3031

32+
private float playbackSpeed = 1f;
33+
3134
private long duration = 0L;
3235

3336
private long currentPosition = 0L;
@@ -254,6 +257,27 @@ public void setIsLooping(boolean value) {
254257
mainHandler.post(() -> player.setRepeatMode(value ? Player.REPEAT_MODE_ALL : Player.REPEAT_MODE_OFF));
255258
}
256259

260+
/**
261+
* @return the playback speed of the video
262+
*/
263+
public float getPlaybackSpeed() {
264+
return playbackSpeed;
265+
}
266+
267+
/**
268+
* Set the playback speed of the video
269+
*
270+
* @param value the playback speed to set (must be greater than 0)
271+
*/
272+
public void setPlaybackSpeed(float value) {
273+
playbackSpeed = Math.max(0.1f, value); // Minimum speed of 0.1x
274+
mainHandler.post(() -> {
275+
if (player != null) {
276+
player.setPlaybackParameters(new PlaybackParameters(playbackSpeed, 1.0f));
277+
}
278+
});
279+
}
280+
257281
/**
258282
* Decode the next frame of the video
259283
*

example/src/VideoPlayerExample.tsx

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Button,
66
PixelRatio,
77
Platform,
8+
Text,
89
View,
910
useWindowDimensions,
1011
} from 'react-native';
@@ -28,11 +29,13 @@ const VideoPlayerExample = () => {
2829
const [loading, setLoading] = useState(true);
2930
const [isPlaying, setIsPlaying] = useState(false);
3031
const [blur, setBlur] = useState(false);
32+
const [playbackSpeed, setPlaybackSpeed] = useState(1.0);
3133

3234
const loadRandomVideo = useCallback(() => {
3335
setLoading(true);
3436
setVideo(null);
3537
setLoadedRanges([]);
38+
setPlaybackSpeed(1.0);
3639
pexelsClient.videos
3740
.popular({ per_page: 1, page: Math.round(Math.random() * 1000) })
3841
.then(
@@ -101,6 +104,7 @@ const VideoPlayerExample = () => {
101104
uri: video?.video_files.find((file) => file.quality === 'hd')?.link ?? null,
102105
autoPlay: true,
103106
isLooping: true,
107+
playbackSpeed,
104108
onReadyToPlay,
105109
onPlayingStatusChange,
106110
onBufferingUpdate,
@@ -209,11 +213,16 @@ const VideoPlayerExample = () => {
209213
minimumTrackTintColor={'#F00'}
210214
/>
211215

212-
<Button
213-
title={isPlaying ? 'Pause' : 'Play'}
214-
onPress={() => (isPlaying ? player?.pause() : player?.play())}
215-
disabled={loading}
216-
/>
216+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
217+
<Button
218+
title={isPlaying ? 'Pause' : 'Play'}
219+
onPress={() => (isPlaying ? player?.pause() : player?.play())}
220+
disabled={loading}
221+
/>
222+
<Text style={{ color: '#000', fontSize: 16, fontWeight: '600' }}>
223+
Speed: {playbackSpeed}x
224+
</Text>
225+
</View>
217226
<Button
218227
title={'Load Random Video'}
219228
onPress={loadRandomVideo}
@@ -223,6 +232,30 @@ const VideoPlayerExample = () => {
223232
title={blur ? 'Unblur' : 'Blur'}
224233
onPress={() => setBlur((blur) => !blur)}
225234
/>
235+
<View
236+
style={{
237+
flexDirection: 'row',
238+
gap: 10,
239+
flexWrap: 'wrap',
240+
justifyContent: 'center',
241+
alignItems: 'center',
242+
}}
243+
>
244+
{[0.5, 0.75, 1.0, 1.25, 1.5, 2.0].map((speed) => (
245+
<Button
246+
key={speed}
247+
title={`${speed}x`}
248+
onPress={() => setPlaybackSpeed(speed)}
249+
color={playbackSpeed === speed ? '#007AFF' : undefined}
250+
disabled={loading}
251+
/>
252+
))}
253+
</View>
254+
<Button
255+
title={`Reset Speed`}
256+
onPress={() => setPlaybackSpeed(1.0)}
257+
disabled={loading}
258+
/>
226259
</View>
227260
</View>
228261
);

ios/RNSVVideoPlayer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
2222
@property(nonatomic, readonly) CMTime currentTime;
2323
@property(nonatomic, readonly) CMTime duration;
2424
@property(nonatomic) float volume;
25+
@property(nonatomic) float playbackSpeed;
2526
@property(nonatomic, readonly) BOOL disposed;
2627
@property(nonatomic, readonly) BOOL isInitialized;
2728
@property(nonatomic, readonly) BOOL isPlaying;

ios/RNSVVideoPlayer.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ - (void)setVolume:(float)volume {
102102
(float)((volume < 0.0) ? 0.0 : ((volume > 1.0) ? 1.0 : volume));
103103
}
104104

105+
- (void)setPlaybackSpeed:(float)playbackSpeed {
106+
_player.rate = MAX(0.1f, playbackSpeed);
107+
}
108+
105109
- (nullable id<MTLTexture>)getNextTextureForTime:(CMTime)time {
106110
id<MTLTexture> texture = NULL;
107111
if ([_videoOutput hasNewPixelBufferForItemTime:time]) {

ios/VideoPlayerHostObject.mm

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("currentTime")));
3939
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("duration")));
4040
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("volume")));
41+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("playbackSpeed")));
4142
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isLooping")));
4243
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isPlaying")));
4344
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("dispose")));
@@ -146,6 +147,12 @@
146147
}
147148
float volume = player.volume;
148149
return jsi::Value(volume);
150+
} else if (propName == "playbackSpeed") {
151+
if (released.test()) {
152+
return jsi::Value(1);
153+
}
154+
float playbackSpeed = player.playbackSpeed;
155+
return jsi::Value(playbackSpeed);
149156
} else if (propName == "isLooping") {
150157
if (released.test()) {
151158
return jsi::Value(false);
@@ -170,6 +177,8 @@
170177
auto propName = propNameId.utf8(runtime);
171178
if (propName == "volume") {
172179
player.volume = value.asNumber();
180+
} else if (propName == "playbackSpeed") {
181+
player.playbackSpeed = value.asNumber();
173182
} else if (propName == "isLooping") {
174183
player.isLooping = value.asBool();
175184
}

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ export type VideoPlayer = {
9494
* The value should be between 0 and 1.
9595
*/
9696
volume: number;
97+
/**
98+
* The playback speed of the video.
99+
* The value should be greater than 0. 1.0 is normal speed, 2.0 is double speed, 0.5 is half speed.
100+
*/
101+
playbackSpeed: number;
97102
/**
98103
* Disposes of the video player.
99104
*/

src/videoPlayer.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ type UseVideoPlayerOptions = {
3838
* The volume of the video.
3939
*/
4040
volume?: number;
41+
/**
42+
* The playback speed of the video.
43+
* The value should be greater than 0. 1.0 is normal speed, 2.0 is double speed, 0.5 is half speed.
44+
*/
45+
playbackSpeed?: number;
4146
/**
4247
* Callback that is called when the video is ready to play.
4348
* @param dimensions The dimensions of the video.
@@ -105,6 +110,7 @@ export const useVideoPlayer = ({
105110
autoPlay = false,
106111
isLooping = false,
107112
volume = 1,
113+
playbackSpeed = 1,
108114
onReadyToPlay,
109115
onBufferingStart,
110116
onBufferingEnd,
@@ -156,6 +162,12 @@ export const useVideoPlayer = ({
156162
}
157163
}, [player, volume]);
158164

165+
useEffect(() => {
166+
if (player) {
167+
player.playbackSpeed = playbackSpeed;
168+
}
169+
}, [player, playbackSpeed]);
170+
159171
useEventListener(player, 'ready', onReadyToPlay);
160172
useEventListener(player, 'bufferingStart', onBufferingStart);
161173
useEventListener(player, 'bufferingEnd', onBufferingEnd);

0 commit comments

Comments
 (0)