mirror of
https://github.com/jellyfin/jellyfin-expo.git
synced 2024-11-23 14:09:41 +00:00
Merge pull request #339 from thornbill/native-audio
Add experimental native audio player
This commit is contained in:
commit
cf20a46163
@ -119,6 +119,7 @@ window.NativeShell = {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
'NativeAudioPlayer',
|
||||
'NativeVideoPlayer'
|
||||
];
|
||||
},
|
||||
|
195
assets/js/plugins/NativeAudioPlayer.staticjs
Normal file
195
assets/js/plugins/NativeAudioPlayer.staticjs
Normal file
@ -0,0 +1,195 @@
|
||||
/**
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
class NativeAudioPlayer {
|
||||
constructor({ events, playbackManager, loading }) {
|
||||
// declare public fields because iOS < 14 does not support them properly
|
||||
this.name = 'ExpoAudioPlayer';
|
||||
this.type = 'mediaplayer';
|
||||
this.id = 'expoaudioplayer';
|
||||
this.priority = -1;
|
||||
this.isLocalPlayer = true;
|
||||
|
||||
this.uri = null;
|
||||
this._isStarting = false;
|
||||
this._isPlaying = false;
|
||||
this.positionMillis = 0;
|
||||
|
||||
// local references to web classes
|
||||
this.events = events;
|
||||
this.playbackManager = playbackManager;
|
||||
this.loading = loading;
|
||||
|
||||
// expose this instance to the global scope
|
||||
window[this.name] = this;
|
||||
}
|
||||
|
||||
// Called by web when the player should be destroyed
|
||||
destroy() {
|
||||
// Reset local player state
|
||||
this.streamInfo = null;
|
||||
this.item = null;
|
||||
this.mediaSource = null;
|
||||
|
||||
this.uri = null;
|
||||
this._isStarting = false;
|
||||
this._isPlaying = false;
|
||||
this.positionMillis = 0;
|
||||
}
|
||||
|
||||
// Report playback status changes to web
|
||||
_reportStatus(status) {
|
||||
// Playback has finished and should be reported as stopped
|
||||
if (status.isFinished) {
|
||||
if (this._isPlaying) {
|
||||
this._isPlaying = false;
|
||||
this._safelyTriggerEvent('stopped');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// The item uri has changed
|
||||
if (status.uri !== this.uri) {
|
||||
this.uri = status.uri;
|
||||
|
||||
if (this.uri === null) {
|
||||
// If the player was closed manually,
|
||||
// we need to tell web to stop playback
|
||||
if (status.didPlayerCloseManually) {
|
||||
this.playbackManager.stop(this);
|
||||
}
|
||||
// Notify web that playback has stopped
|
||||
this._safelyTriggerEvent('stopped');
|
||||
} else {
|
||||
// Set flag used by web to show playback has started
|
||||
this.streamInfo.started = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Playback has stopped don't report events
|
||||
if (this.uri === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The playing state has changed
|
||||
if (status.isPlaying !== this._isPlaying) {
|
||||
this._isPlaying = status.isPlaying;
|
||||
|
||||
if (this._isStarting) {
|
||||
// Send 'playing' event when playback initially starts
|
||||
this._safelyTriggerEvent('playing');
|
||||
this._isStarting = false;
|
||||
} else {
|
||||
// If playback already started send 'pause' or 'unpause' event
|
||||
this._safelyTriggerEvent(this._isPlaying ? 'unpause' : 'pause');
|
||||
}
|
||||
}
|
||||
|
||||
// The playback position has changed
|
||||
if (status.positionMillis !== this.positionMillis) {
|
||||
this.positionMillis = status.positionMillis;
|
||||
this._safelyTriggerEvent('timeupdate');
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger events in web and handle any exceptions
|
||||
_safelyTriggerEvent(event) {
|
||||
try {
|
||||
this.events.trigger(this, event);
|
||||
} catch (ex) {
|
||||
console.error(ex.name + '\n' + ex.message);
|
||||
}
|
||||
}
|
||||
|
||||
// This player only supports audio
|
||||
canPlayMediaType(mediaType) {
|
||||
return mediaType === 'Audio';
|
||||
}
|
||||
|
||||
// Any audio items are supported when enabled
|
||||
canPlayItem() {
|
||||
return window.ExpoAppSettings.isExperimentalNativeAudioPlayerEnabled;
|
||||
}
|
||||
|
||||
// Returns the currently playing item
|
||||
currentItem() {
|
||||
return this.item;
|
||||
}
|
||||
|
||||
// Returns the url of the currently playing item (required by web for the "remote control" screen)
|
||||
currentSrc() {
|
||||
return (this.streamInfo && this.streamInfo.url) || '';
|
||||
}
|
||||
|
||||
// Returns the current media source
|
||||
currentMediaSource() {
|
||||
return this.mediaSource;
|
||||
}
|
||||
|
||||
// Returns the current player position in ms
|
||||
currentTime() {
|
||||
return this.positionMillis;
|
||||
}
|
||||
|
||||
// Dummy method (required by web for live tv)
|
||||
duration() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dummy method (required by web)
|
||||
isMuted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return the playing state (required by web)
|
||||
isPlaying() {
|
||||
return this._isPlaying;
|
||||
}
|
||||
|
||||
// Return the paused state (required by web)
|
||||
paused() {
|
||||
return !this._isPlaying;
|
||||
}
|
||||
|
||||
// Called by web to begin playback
|
||||
play(streamInfo) {
|
||||
this._isStarting = true;
|
||||
postExpoEvent(this.name + '.play', streamInfo);
|
||||
|
||||
// Save the currently playing item
|
||||
this.streamInfo = streamInfo;
|
||||
this.item = streamInfo.item;
|
||||
this.mediaSource = streamInfo.mediaSource;
|
||||
|
||||
// We don't know when the audio player is actually
|
||||
// displayed so delay hiding the loading indicator
|
||||
// for a few seconds
|
||||
return new Promise(resolve => window.setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
playPause() {
|
||||
postExpoEvent(this.name + '.playPause');
|
||||
}
|
||||
|
||||
stop(destroy) {
|
||||
postExpoEvent(this.name + '.stop', destroy);
|
||||
if (destroy) {
|
||||
this.destroy();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Dummy method (required by web)
|
||||
volume() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
getDeviceProfile() {
|
||||
return Promise.resolve(window.ExpoVideoProfile || {});
|
||||
}
|
||||
}
|
||||
|
||||
window.NativeAudioPlayer = () => NativeAudioPlayer;
|
104
components/AudioPlayer.js
Normal file
104
components/AudioPlayer.js
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Audio } from 'expo-av';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import MediaTypes from '../constants/MediaTypes';
|
||||
import { useStores } from '../hooks/useStores';
|
||||
import { msToTicks } from '../utils/Time';
|
||||
|
||||
const AudioPlayer = observer(() => {
|
||||
const { rootStore } = useStores();
|
||||
|
||||
const [ player, setPlayer ] = useState();
|
||||
|
||||
// Set the audio mode when the audio player is created
|
||||
useEffect(() => {
|
||||
Audio.setAudioModeAsync({
|
||||
staysActiveInBackground: true,
|
||||
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX,
|
||||
playThroughEarpieceAndroid: false,
|
||||
shouldDuckAndroid: true,
|
||||
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
|
||||
playsInSilentModeIOS: true
|
||||
});
|
||||
|
||||
return () => {
|
||||
player?.stopAsync();
|
||||
player?.unloadAsync();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update the player when media type or uri changes
|
||||
useEffect(() => {
|
||||
const createPlayer = async ({ uri, positionMillis }) => {
|
||||
const { isLoaded } = await player?.getStatusAsync() || { isLoaded: false };
|
||||
if (isLoaded) {
|
||||
// If the player is already loaded, seek to the correct position
|
||||
player.setPositionAsync(positionMillis);
|
||||
} else {
|
||||
// Create the player if not already loaded
|
||||
const { sound } = await Audio.Sound.createAsync({
|
||||
uri
|
||||
}, {
|
||||
positionMillis,
|
||||
shouldPlay: true
|
||||
}, ({
|
||||
isPlaying,
|
||||
positionMillis: positionMs,
|
||||
didJustFinish
|
||||
}) => {
|
||||
if (
|
||||
didJustFinish === undefined ||
|
||||
isPlaying === undefined ||
|
||||
positionMs === undefined ||
|
||||
rootStore.mediaStore.isFinished
|
||||
) {
|
||||
return;
|
||||
}
|
||||
rootStore.mediaStore.isFinished = didJustFinish;
|
||||
rootStore.mediaStore.isPlaying = isPlaying;
|
||||
rootStore.mediaStore.positionTicks = msToTicks(positionMs);
|
||||
});
|
||||
setPlayer(sound);
|
||||
}
|
||||
};
|
||||
|
||||
if (rootStore.mediaStore.type === MediaTypes.Audio) {
|
||||
createPlayer({
|
||||
uri: rootStore.mediaStore.uri,
|
||||
positionMillis: rootStore.mediaStore.positionMillis
|
||||
});
|
||||
}
|
||||
}, [ rootStore.mediaStore.type, rootStore.mediaStore.uri ]);
|
||||
|
||||
// Update the play/pause state when the store indicates it should
|
||||
useEffect(() => {
|
||||
if (rootStore.mediaStore.type === MediaTypes.Audio && rootStore.mediaStore.shouldPlayPause) {
|
||||
if (rootStore.mediaStore.isPlaying) {
|
||||
player?.pauseAsync();
|
||||
} else {
|
||||
player?.playAsync();
|
||||
}
|
||||
rootStore.mediaStore.shouldPlayPause = false;
|
||||
}
|
||||
}, [ rootStore.mediaStore.shouldPlayPause ]);
|
||||
|
||||
// Stop the player when the store indicates it should stop playback
|
||||
useEffect(() => {
|
||||
if (rootStore.mediaStore.type === MediaTypes.Audio && rootStore.mediaStore.shouldStop) {
|
||||
player?.stopAsync();
|
||||
player?.unloadAsync();
|
||||
rootStore.mediaStore.shouldStop = false;
|
||||
}
|
||||
}, [ rootStore.mediaStore.shouldStop ]);
|
||||
|
||||
return <></>;
|
||||
});
|
||||
|
||||
export default AudioPlayer;
|
@ -37,7 +37,8 @@ window.ExpoAppInfo = {
|
||||
|
||||
window.ExpoAppSettings = {
|
||||
isPluginSupported: ${isPluginSupported},
|
||||
isNativeVideoPlayerEnabled: ${rootStore.settingStore.isNativeVideoPlayerEnabled}
|
||||
isNativeVideoPlayerEnabled: ${rootStore.settingStore.isNativeVideoPlayerEnabled},
|
||||
isExperimentalNativeAudioPlayerEnabled: ${rootStore.settingStore.isExperimentalNativeAudioPlayerEnabled}
|
||||
};
|
||||
|
||||
window.ExpoVideoProfile = ${JSON.stringify(getDeviceProfile({ enableFmp4: rootStore.settingStore.isFmp4Enabled }))};
|
||||
@ -49,6 +50,7 @@ function postExpoEvent(event, data) {
|
||||
}));
|
||||
}
|
||||
|
||||
${StaticScriptLoader.scripts.NativeAudioPlayer}
|
||||
${StaticScriptLoader.scripts.NativeVideoPlayer}
|
||||
|
||||
${StaticScriptLoader.scripts.NativeShell}
|
||||
@ -63,6 +65,10 @@ true;
|
||||
const onRefresh = () => {
|
||||
// Disable pull to refresh when in fullscreen
|
||||
if (rootStore.isFullscreen) return;
|
||||
|
||||
// Stop media playback in native players
|
||||
rootStore.mediaStore.shouldStop = true;
|
||||
|
||||
setIsRefreshing(true);
|
||||
ref.current?.reload();
|
||||
setIsRefreshing(false);
|
||||
@ -97,15 +103,19 @@ true;
|
||||
deactivateKeepAwake();
|
||||
}
|
||||
break;
|
||||
case 'ExpoAudioPlayer.play':
|
||||
case 'ExpoVideoPlayer.play':
|
||||
rootStore.mediaStore.type = MediaTypes.Video;
|
||||
rootStore.mediaStore.type = event === 'ExpoAudioPlayer.play' ? MediaTypes.Audio : MediaTypes.Video;
|
||||
rootStore.mediaStore.uri = data.url;
|
||||
rootStore.mediaStore.posterUri = data.backdropUrl;
|
||||
rootStore.mediaStore.backdropUri = data.backdropUrl;
|
||||
rootStore.mediaStore.isFinished = false;
|
||||
rootStore.mediaStore.positionTicks = data.playerStartPositionTicks;
|
||||
break;
|
||||
case 'ExpoAudioPlayer.playPause':
|
||||
case 'ExpoVideoPlayer.playPause':
|
||||
rootStore.mediaStore.shouldPlayPause = true;
|
||||
break;
|
||||
case 'ExpoAudioPlayer.stop':
|
||||
case 'ExpoVideoPlayer.stop':
|
||||
rootStore.mediaStore.shouldStop = true;
|
||||
break;
|
||||
|
@ -86,7 +86,7 @@ const VideoPlayer = observer(() => {
|
||||
<Video
|
||||
ref={player}
|
||||
usePoster
|
||||
posterSource={{ uri: rootStore.mediaStore.posterUri }}
|
||||
posterSource={{ uri: rootStore.mediaStore.backdropUri }}
|
||||
resizeMode='contain'
|
||||
useNativeControls
|
||||
onReadyForDisplay={openFullscreen}
|
||||
|
20
components/__tests__/AudioPlayer.test.js
Normal file
20
components/__tests__/AudioPlayer.test.js
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
import AudioPlayer from '../AudioPlayer';
|
||||
|
||||
describe('AudioPlayer', () => {
|
||||
it('should render correctly', () => {
|
||||
const { toJSON } = render(
|
||||
<AudioPlayer />
|
||||
);
|
||||
|
||||
expect(toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AudioPlayer should render correctly 1`] = `null`;
|
@ -50,7 +50,8 @@ window.ExpoAppInfo = {
|
||||
|
||||
window.ExpoAppSettings = {
|
||||
isPluginSupported: true,
|
||||
isNativeVideoPlayerEnabled: undefined
|
||||
isNativeVideoPlayerEnabled: undefined,
|
||||
isExperimentalNativeAudioPlayerEnabled: undefined
|
||||
};
|
||||
|
||||
window.ExpoVideoProfile = {};
|
||||
@ -68,6 +69,7 @@ function postExpoEvent(event, data) {
|
||||
|
||||
|
||||
|
||||
|
||||
window.onerror = console.error;
|
||||
|
||||
true;
|
||||
|
@ -4,13 +4,19 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { action } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useContext } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Text, ThemeContext } from 'react-native-elements';
|
||||
import { FlatList, StyleSheet } from 'react-native';
|
||||
import { ThemeContext } from 'react-native-elements';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
import SwitchListItem from '../components/SwitchListItem';
|
||||
|
||||
import { useStores } from '../hooks/useStores';
|
||||
|
||||
const DevSettingsScreen = observer(() => {
|
||||
const { rootStore } = useStores();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
@ -21,9 +27,26 @@ const DevSettingsScreen = observer(() => {
|
||||
}}
|
||||
edges={[ 'right', 'left' ]}
|
||||
>
|
||||
<Text>
|
||||
This is a place where development features can be enabled.
|
||||
</Text>
|
||||
<FlatList
|
||||
data={[
|
||||
{
|
||||
key: 'experimental-audio-player-switch',
|
||||
title: 'Native Audio Player',
|
||||
badge: {
|
||||
value: 'Experimental',
|
||||
status: 'error'
|
||||
},
|
||||
value: rootStore.settingStore.isExperimentalNativeAudioPlayerEnabled,
|
||||
onValueChange: action(value => {
|
||||
rootStore.settingStore.isExperimentalNativeAudioPlayerEnabled = value;
|
||||
rootStore.isReloadRequired = true;
|
||||
})
|
||||
}
|
||||
]}
|
||||
renderItem={SwitchListItem}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
});
|
||||
@ -31,6 +54,9 @@ const DevSettingsScreen = observer(() => {
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
listContainer: {
|
||||
marginTop: 1
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -11,10 +11,12 @@ import { BackHandler, Platform, StyleSheet, View } from 'react-native';
|
||||
import { ThemeContext } from 'react-native-elements';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import AudioPlayer from '../components/AudioPlayer';
|
||||
import ErrorView from '../components/ErrorView';
|
||||
import NativeShellWebView from '../components/NativeShellWebView';
|
||||
import VideoPlayer from '../components/VideoPlayer';
|
||||
import Colors from '../constants/Colors';
|
||||
import MediaTypes from '../constants/MediaTypes';
|
||||
import Screens from '../constants/Screens';
|
||||
import { useStores } from '../hooks/useStores';
|
||||
import { getIconName } from '../utils/Icons';
|
||||
@ -58,14 +60,27 @@ const HomeScreen = observer(() => {
|
||||
|
||||
// Report media updates to the video plugin
|
||||
useEffect(() => {
|
||||
webview.current?.injectJavaScript(`window.ExpoVideoPlayer && window.ExpoVideoPlayer._reportStatus(${JSON.stringify({
|
||||
const status = {
|
||||
didPlayerCloseManually: rootStore.didPlayerCloseManually,
|
||||
uri: rootStore.mediaStore.uri,
|
||||
isFinished: rootStore.mediaStore.isFinished,
|
||||
isPlaying: rootStore.mediaStore.isPlaying,
|
||||
positionTicks: rootStore.mediaStore.positionTicks,
|
||||
positionMillis: rootStore.mediaStore.positionMillis
|
||||
})});`);
|
||||
}, [ rootStore.mediaStore.uri, rootStore.mediaStore.isPlaying, rootStore.mediaStore.positionTicks ]);
|
||||
};
|
||||
|
||||
if (rootStore.mediaStore.type === MediaTypes.Audio) {
|
||||
webview.current?.injectJavaScript(`window.ExpoAudioPlayer && window.ExpoAudioPlayer._reportStatus(${JSON.stringify(status)});`);
|
||||
} else if (rootStore.mediaStore.type === MediaTypes.Video) {
|
||||
webview.current?.injectJavaScript(`window.ExpoVideoPlayer && window.ExpoVideoPlayer._reportStatus(${JSON.stringify(status)});`);
|
||||
}
|
||||
}, [
|
||||
rootStore.mediaStore.type,
|
||||
rootStore.mediaStore.uri,
|
||||
rootStore.mediaStore.isFinished,
|
||||
rootStore.mediaStore.isPlaying,
|
||||
rootStore.mediaStore.positionTicks
|
||||
]);
|
||||
|
||||
// Clear the error state when the active server changes
|
||||
useEffect(() => {
|
||||
@ -187,6 +202,7 @@ const HomeScreen = observer(() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
/>
|
||||
<AudioPlayer/>
|
||||
<VideoPlayer/>
|
||||
</>
|
||||
) : (
|
||||
|
@ -15,14 +15,226 @@ exports[`DevSettingsScreen should render correctly 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
<RCTScrollView
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"color": "#242424",
|
||||
"marginTop": 1,
|
||||
}
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"badge": Object {
|
||||
"status": "error",
|
||||
"value": "Experimental",
|
||||
},
|
||||
"key": "experimental-audio-player-switch",
|
||||
"onValueChange": [Function],
|
||||
"title": "Native Audio Player",
|
||||
"value": false,
|
||||
},
|
||||
]
|
||||
}
|
||||
disableVirtualization={false}
|
||||
getItem={[Function]}
|
||||
getItemCount={[Function]}
|
||||
horizontal={false}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
maxToRenderPerBatch={10}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onLayout={[Function]}
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
removeClippedSubviews={false}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
showsVerticalScrollIndicator={false}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
viewabilityConfigCallbackPairs={Array []}
|
||||
windowSize={21}
|
||||
>
|
||||
This is a place where development features can be enabled.
|
||||
</Text>
|
||||
<View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={null}
|
||||
>
|
||||
<View
|
||||
testID="switch-list-item"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderBottomWidth": 0.5,
|
||||
"borderColor": "#bcbbc1",
|
||||
"borderTopWidth": 0.5,
|
||||
"flexDirection": "row",
|
||||
"padding": 14,
|
||||
}
|
||||
}
|
||||
testID="padView"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "flex-start",
|
||||
"flex": 1,
|
||||
"justifyContent": "center",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"colors": Object {
|
||||
"black": "#242424",
|
||||
"disabled": "hsl(208, 8%, 90%)",
|
||||
"divider": "#bcbbc1",
|
||||
"error": "#ff190c",
|
||||
"grey0": "#393e42",
|
||||
"grey1": "#43484d",
|
||||
"grey2": "#5e6977",
|
||||
"grey3": "#86939e",
|
||||
"grey4": "#bdc6cf",
|
||||
"grey5": "#e1e8ee",
|
||||
"greyOutline": "#bbb",
|
||||
"platform": Object {
|
||||
"android": Object {
|
||||
"error": "#f44336",
|
||||
"grey": "rgba(0, 0, 0, 0.54)",
|
||||
"primary": "#2196f3",
|
||||
"searchBg": "#dcdce1",
|
||||
"secondary": "#9C27B0",
|
||||
"success": "#4caf50",
|
||||
"warning": "#ffeb3b",
|
||||
},
|
||||
"default": Object {
|
||||
"error": "#ff3b30",
|
||||
"grey": "#7d7d7d",
|
||||
"primary": "#007aff",
|
||||
"searchBg": "#dcdce1",
|
||||
"secondary": "#5856d6",
|
||||
"success": "#4cd964",
|
||||
"warning": "#ffcc00",
|
||||
},
|
||||
"ios": Object {
|
||||
"error": "#ff3b30",
|
||||
"grey": "#7d7d7d",
|
||||
"primary": "#007aff",
|
||||
"searchBg": "#dcdce1",
|
||||
"secondary": "#5856d6",
|
||||
"success": "#4cd964",
|
||||
"warning": "#ffcc00",
|
||||
},
|
||||
"web": Object {
|
||||
"error": "#ff190c",
|
||||
"grey": "#393e42",
|
||||
"primary": "#2089dc",
|
||||
"searchBg": "#303337",
|
||||
"secondary": "#ca71eb",
|
||||
"success": "#52c41a",
|
||||
"warning": "#faad14",
|
||||
},
|
||||
},
|
||||
"primary": "#2089dc",
|
||||
"searchBg": "#303337",
|
||||
"secondary": "#ca71eb",
|
||||
"success": "#52c41a",
|
||||
"warning": "#faad14",
|
||||
"white": "#ffffff",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "transparent",
|
||||
"color": "#242424",
|
||||
"fontSize": 17,
|
||||
}
|
||||
}
|
||||
testID="title"
|
||||
>
|
||||
Native Audio Player
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"marginStart": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"alignSelf": "center",
|
||||
"backgroundColor": "#ff190c",
|
||||
"borderColor": "#fff",
|
||||
"borderRadius": 9,
|
||||
"borderWidth": 0.5,
|
||||
"height": 18,
|
||||
"justifyContent": "center",
|
||||
"minWidth": 18,
|
||||
}
|
||||
}
|
||||
testID="badge"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
"fontSize": 12,
|
||||
"paddingHorizontal": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
Experimental
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"paddingLeft": 16,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<RCTSwitch
|
||||
accessibilityRole="switch"
|
||||
onChange={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 31,
|
||||
"width": 51,
|
||||
}
|
||||
}
|
||||
testID="switch"
|
||||
value={false}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
@ -19,6 +19,11 @@ export default class MediaStore {
|
||||
*/
|
||||
uri
|
||||
|
||||
/**
|
||||
* Has media playback finished
|
||||
*/
|
||||
isFinished = false
|
||||
|
||||
/**
|
||||
* Is the media currently playing
|
||||
*/
|
||||
@ -30,9 +35,9 @@ export default class MediaStore {
|
||||
positionTicks = 0
|
||||
|
||||
/**
|
||||
* The URI of the poster image of the current media item
|
||||
* The URI of the backdrop image of the current media item
|
||||
*/
|
||||
posterUri
|
||||
backdropUri
|
||||
|
||||
/**
|
||||
* The player should toggle the play/pause state
|
||||
@ -51,9 +56,10 @@ export default class MediaStore {
|
||||
reset() {
|
||||
this.type = null;
|
||||
this.uri = null;
|
||||
this.isFinished = false;
|
||||
this.isPlaying = false;
|
||||
this.positionTicks = 0;
|
||||
this.posterUri = null;
|
||||
this.backdropUri = null;
|
||||
this.shouldPlayPause = false;
|
||||
this.shouldStop = false;
|
||||
}
|
||||
@ -62,10 +68,11 @@ export default class MediaStore {
|
||||
decorate(MediaStore, {
|
||||
type: [ ignore, observable ],
|
||||
uri: [ ignore, observable ],
|
||||
isFinished: [ ignore, observable ],
|
||||
isPlaying: [ ignore, observable ],
|
||||
positionTicks: [ ignore, observable ],
|
||||
positionMillis: computed,
|
||||
posterUri: [ ignore, observable ],
|
||||
backdropUri: [ ignore, observable ],
|
||||
shouldPlayPause: [ ignore, observable ],
|
||||
shouldStop: [ ignore, observable ],
|
||||
reset: action
|
||||
|
@ -61,6 +61,11 @@ export default class SettingStore {
|
||||
*/
|
||||
isFmp4Enabled = false;
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL: Is the native audio player enabled
|
||||
*/
|
||||
isExperimentalNativeAudioPlayerEnabled = false
|
||||
|
||||
get theme() {
|
||||
const id = this.isSystemThemeEnabled && this.systemThemeId && this.systemThemeId !== 'no-preference' ? this.systemThemeId : this.themeId;
|
||||
return Themes[id] || Themes.dark;
|
||||
@ -76,6 +81,8 @@ export default class SettingStore {
|
||||
this.isSystemThemeEnabled = false;
|
||||
this.isNativeVideoPlayerEnabled = false;
|
||||
this.isFmp4Enabled = false;
|
||||
|
||||
this.isExperimentalNativeAudioPlayerEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,6 +96,7 @@ decorate(SettingStore, {
|
||||
isSystemThemeEnabled: observable,
|
||||
isNativeVideoPlayerEnabled: observable,
|
||||
isFmp4Enabled: observable,
|
||||
isExperimentalNativeAudioPlayerEnabled: observable,
|
||||
theme: computed,
|
||||
reset: action
|
||||
});
|
||||
|
@ -12,10 +12,11 @@ describe('MediaStore', () => {
|
||||
|
||||
expect(store.type).toBeUndefined();
|
||||
expect(store.uri).toBeUndefined();
|
||||
expect(store.isFinished).toBe(false);
|
||||
expect(store.isPlaying).toBe(false);
|
||||
expect(store.positionTicks).toBe(0);
|
||||
expect(store.positionMillis).toBe(0);
|
||||
expect(store.posterUri).toBeUndefined();
|
||||
expect(store.backdropUri).toBeUndefined();
|
||||
expect(store.shouldPlayPause).toBe(false);
|
||||
expect(store.shouldStop).toBe(false);
|
||||
});
|
||||
@ -24,28 +25,31 @@ describe('MediaStore', () => {
|
||||
const store = new MediaStore();
|
||||
store.type = MediaTypes.Video;
|
||||
store.uri = 'https://foobar';
|
||||
store.isFinished = true;
|
||||
store.isPlaying = true;
|
||||
store.positionTicks = 3423000;
|
||||
store.posterUri = 'https://foobar';
|
||||
store.backdropUri = 'https://foobar';
|
||||
store.shouldPlayPause = true;
|
||||
store.shouldStop = true;
|
||||
|
||||
expect(store.type).toBe(MediaTypes.Video);
|
||||
expect(store.uri).toBe('https://foobar');
|
||||
expect(store.isFinished).toBe(true);
|
||||
expect(store.isPlaying).toBe(true);
|
||||
expect(store.positionTicks).toBe(3423000);
|
||||
expect(store.positionMillis).toBe(342.3);
|
||||
expect(store.posterUri).toBe('https://foobar');
|
||||
expect(store.backdropUri).toBe('https://foobar');
|
||||
expect(store.shouldPlayPause).toBe(true);
|
||||
expect(store.shouldStop).toBe(true);
|
||||
|
||||
store.reset();
|
||||
expect(store.type).toBeNull();
|
||||
expect(store.uri).toBeNull();
|
||||
expect(store.isFinished).toBe(false);
|
||||
expect(store.isPlaying).toBe(false);
|
||||
expect(store.positionTicks).toBe(0);
|
||||
expect(store.positionMillis).toBe(0);
|
||||
expect(store.posterUri).toBeNull();
|
||||
expect(store.backdropUri).toBeNull();
|
||||
expect(store.shouldPlayPause).toBe(false);
|
||||
expect(store.shouldStop).toBe(false);
|
||||
});
|
||||
|
@ -13,6 +13,7 @@ const loadStaticFile = async (asset) => {
|
||||
|
||||
class Loader {
|
||||
scripts = {
|
||||
NativeAudioPlayer: '',
|
||||
NativeVideoPlayer: '',
|
||||
NativeShell: '',
|
||||
ExpoRouterShim: ''
|
||||
@ -20,6 +21,7 @@ class Loader {
|
||||
|
||||
async load() {
|
||||
// Load NativeShell plugins
|
||||
this.scripts.NativeAudioPlayer = await loadStaticFile(require('../assets/js/plugins/NativeAudioPlayer.staticjs'));
|
||||
this.scripts.NativeVideoPlayer = await loadStaticFile(require('../assets/js/plugins/NativeVideoPlayer.staticjs'));
|
||||
// Load the NativeShell
|
||||
this.scripts.NativeShell = await loadStaticFile(require('../assets/js/NativeShell.staticjs'));
|
||||
|
Loading…
Reference in New Issue
Block a user