Skip to content

Commit

Permalink
[expo-video] Add events (#27632)
Browse files Browse the repository at this point in the history
# Why

Adds `statusChange`, `playingChange`, `playbackRateChange`,
`volumeChange`, `playToEnd`, `sourceChange` events for Android and iOS.

# How

Used the new `SharedObject.sendEvent` methods to send the events

# Test Plan

Tested in BareExpo on iOS and Android
  • Loading branch information
behenate committed Mar 25, 2024
1 parent 852e1fb commit f14140e
Show file tree
Hide file tree
Showing 44 changed files with 862 additions and 170 deletions.
56 changes: 45 additions & 11 deletions apps/native-component-list/src/screens/Video/VideoScreen.tsx
Expand Up @@ -2,7 +2,7 @@ import Slider from '@react-native-community/slider';
import { Picker } from '@react-native-picker/picker';
import SegmentedControl from '@react-native-segmented-control/segmented-control';
import { Platform } from 'expo-modules-core';
import { useVideoPlayer, VideoView, VideoSource } from 'expo-video';
import { useVideoPlayer, VideoView, VideoSource, VideoPlayerEvents } from 'expo-video';
import React, { useCallback, useEffect, useRef } from 'react';
import { PixelRatio, ScrollView, StyleSheet, Text, View } from 'react-native';

Expand Down Expand Up @@ -30,7 +30,14 @@ const androidDrmSource: VideoSource = {
const videoLabels: string[] = ['Big Buck Bunny', 'Elephants Dream'];
const videoSources: VideoSource[] = [bigBuckBunnySource, elephantsDreamSource];
const playbackRates: number[] = [0.25, 0.5, 1, 1.5, 2, 16];

const eventsToListen: (keyof VideoPlayerEvents)[] = [
'statusChange',
'playingChange',
'playbackRateChange',
'volumeChange',
'playToEnd',
'sourceChange',
];
if (Platform.OS === 'android') {
videoLabels.push('Tears of Steel (DRM protected)');
videoSources.push(androidDrmSource);
Expand All @@ -47,11 +54,18 @@ export default function VideoScreen() {
const [staysActiveInBackground, setStaysActiveInBackground] = React.useState(false);
const [loop, setLoop] = React.useState(false);
const [playbackRateIndex, setPlaybackRateIndex] = React.useState(2);
const [shouldCorrectPitch, setCorrectsPitch] = React.useState(true);
const [preservePitch, setPreservePitch] = React.useState(true);
const [volume, setVolume] = React.useState(1);
const [currentSource, setCurrentSource] = React.useState(videoSources[0]);
const [logEvents, setLogEvents] = React.useState(false);

const player = useVideoPlayer(currentSource);
const player = useVideoPlayer(currentSource, (player) => {
player.volume = volume;
player.loop = loop;
player.preservesPitch = preservePitch;
player.staysActiveInBackground = staysActiveInBackground;
player.play();
});

const enterFullscreen = useCallback(() => {
ref.current?.enterFullscreen();
Expand Down Expand Up @@ -102,17 +116,30 @@ export default function VideoScreen() {
);

const updatePreservesPitch = useCallback(
(correctPitch: boolean) => {
player.preservesPitch = correctPitch;
setCorrectsPitch(correctPitch);
(preservesPitch: boolean) => {
player.preservesPitch = preservesPitch;
setPreservePitch(preservesPitch);
},
[player]
);

useEffect(() => {
player.play();
player.preservesPitch = shouldCorrectPitch;
}, [player]);
if (logEvents) {
eventsToListen.forEach((eventName) => {
player.addListener(eventName, (newValue: any, _: any, error: any) => {
console.log(
`${eventName}: ${JSON.stringify(newValue)} ${(error && JSON.stringify(error)) ?? ''}`
);
});
});
}

return () => {
eventsToListen.forEach((eventName) => {
player.removeAllListeners(eventName);
});
};
}, [logEvents, player]);

return (
<View style={styles.contentContainer}>
Expand Down Expand Up @@ -234,11 +261,18 @@ export default function VideoScreen() {
<View style={styles.row}>
<TitledSwitch
title="Should correct pitch"
value={shouldCorrectPitch}
value={preservePitch}
setValue={updatePreservesPitch}
style={styles.switch}
titleStyle={styles.switchTitle}
/>
<TitledSwitch
title="Log events"
value={logEvents}
setValue={setLogEvents}
style={styles.switch}
titleStyle={styles.switchTitle}
/>
</View>
</ScrollView>
</View>
Expand Down
Expand Up @@ -26,12 +26,12 @@ open class SharedObject(appContext: AppContext? = null) {
)
}

fun sendEvent(eventName: String, vararg args: Any) {
fun sendEvent(eventName: String, vararg args: Any?) {
val jsThis = getJavaScriptObject() ?: return

try {
jsThis.getProperty("emit")
.getFunction<Unit>()
.getFunction<Unit?>()
.invoke(
eventName,
*args,
Expand Down
1 change: 1 addition & 0 deletions packages/expo-video/CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@

### 🎉 New features

- Add support for events on Android and iOS. ([#27632](https://github.com/expo/expo/pull/27632) by [@behenate](https://github.com/behenate))
- Add support for `loop`, `playbackRate`, `preservesPitch` and `currentTime` properties. ([#27367](https://github.com/expo/expo/pull/27367) by [@behenate](https://github.com/behenate))
- Add background playback support. ([#27110](https://github.com/expo/expo/pull/27110) by [@behenate](https://github.com/behenate))
- Add DRM support for Android and iOS. ([#26465](https://github.com/expo/expo/pull/26465) by [@behenate](https://github.com/behenate))
Expand Down
Expand Up @@ -20,3 +20,6 @@ internal class PictureInPictureUnsupportedException :

internal class UnsupportedDRMTypeException(type: DRMType) :
CodedException("DRM type `$type` is not supported on Android")

internal class PlaybackException(reason: String?, cause: Throwable? = null) :
CodedException("A playback exception has occurred: ${reason ?: "reason unknown"}", cause)
@@ -1,7 +1,10 @@
package expo.modules.video

import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import expo.modules.video.records.VideoSource
import java.lang.ref.WeakReference

// Helper class used to keep track of all existing VideoViews and VideoPlayers
@OptIn(UnstableApi::class)
Expand All @@ -14,6 +17,9 @@ object VideoManager {
// Keeps track of all existing VideoPlayers, and whether they are attached to a VideoView
private var videoPlayersToVideoViews = mutableMapOf<VideoPlayer, MutableList<VideoView>>()

// Keeps track of all existing MediaItems and their corresponding VideoSources. Used for recognizing source of MediaItems.
private var mediaItemsToVideoSources = mutableMapOf<String, WeakReference<VideoSource>>()

fun registerVideoView(videoView: VideoView) {
videoViews[videoView.id] = videoView
}
Expand All @@ -34,6 +40,17 @@ object VideoManager {
videoPlayersToVideoViews.remove(videoPlayer)
}

fun registerVideoSourceToMediaItem(mediaItem: MediaItem, videoSource: VideoSource) {
mediaItemsToVideoSources[mediaItem.mediaId] = WeakReference(videoSource)
}

fun getVideoSourceFromMediaItem(mediaItem: MediaItem?): VideoSource? {
if (mediaItem == null) {
return null
}
return mediaItemsToVideoSources[mediaItem.mediaId]?.get()
}

fun onVideoPlayerAttachedToView(videoPlayer: VideoPlayer, videoView: VideoView) {
if (videoPlayersToVideoViews[videoPlayer]?.contains(videoView) == true) {
return
Expand Down
Expand Up @@ -139,19 +139,16 @@ class VideoModule : Module() {

Class(VideoPlayer::class) {
Constructor { source: VideoSource ->
VideoPlayer(activity.applicationContext, appContext, source.toMediaItem())
val mediaItem = source.toMediaItem()
VideoManager.registerVideoSourceToMediaItem(mediaItem, source)
VideoPlayer(activity.applicationContext, appContext, mediaItem)
}

Property("playing")
.get { ref: VideoPlayer ->
ref.playing
}

Property("isLoading")
.get { ref: VideoPlayer ->
ref.isLoading
}

Property("muted")
.get { ref: VideoPlayer ->
ref.muted
Expand Down Expand Up @@ -209,6 +206,11 @@ class VideoModule : Module() {
}
}

Property("status")
.get { ref: VideoPlayer ->
ref.status
}

Property("staysActiveInBackground")
.get { ref: VideoPlayer ->
ref.staysActiveInBackground
Expand Down Expand Up @@ -249,9 +251,11 @@ class VideoModule : Module() {
} else {
VideoSource(source.get(String::class))
}
val mediaItem = videoSource.toMediaItem()
VideoManager.registerVideoSourceToMediaItem(mediaItem, videoSource)

appContext.mainQueue.launch {
ref.player.setMediaItem(videoSource.toMediaItem())
ref.player.setMediaItem(mediaItem)
}
}

Expand Down

0 comments on commit f14140e

Please sign in to comment.