mirror of
https://github.com/jellyfin/jellyfin-expo.git
synced 2024-11-23 05:59:39 +00:00
Add native audio player
This commit is contained in:
parent
4986e0b23c
commit
0c09b97962
@ -119,6 +119,7 @@ window.NativeShell = {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
// 'NativeAudioPlayer',
|
||||
'NativeVideoPlayer'
|
||||
];
|
||||
},
|
||||
|
174
assets/js/plugins/NativeAudioPlayer.staticjs
Normal file
174
assets/js/plugins/NativeAudioPlayer.staticjs
Normal file
@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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._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._isPlaying = false;
|
||||
this.positionMillis = 0;
|
||||
}
|
||||
|
||||
// Report playback status changes to web
|
||||
_reportStatus(status) {
|
||||
// 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;
|
||||
this._safelyTriggerEvent(this._isPlaying ? 'playing' : '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.isNativeAudioPlayerEnabled;
|
||||
// FIXME: Add setting for this also
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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 1;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Dummy method (required by web)
|
||||
volume() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
getDeviceProfile() {
|
||||
return Promise.resolve(window.ExpoVideoProfile || {});
|
||||
}
|
||||
}
|
||||
|
||||
window.NativeAudioPlayer = () => NativeAudioPlayer;
|
72
components/AudioPlayer.js
Normal file
72
components/AudioPlayer.js
Normal file
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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({
|
||||
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX,
|
||||
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
|
||||
playsInSilentModeIOS: true
|
||||
});
|
||||
|
||||
return () => {
|
||||
player?.unloadAsync();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update the player when media type or uri changes
|
||||
useEffect(() => {
|
||||
if (rootStore.mediaStore.type === MediaTypes.Audio) {
|
||||
setPlayer(Audio.Sound.createAsync({
|
||||
uri: rootStore.mediaStore.uri
|
||||
}, {
|
||||
positionMillis: rootStore.mediaStore.positionMillis,
|
||||
shouldPlay: true
|
||||
}, ({ isPlaying, positionMillis }) => {
|
||||
rootStore.mediaStore.isPlaying = isPlaying;
|
||||
rootStore.mediaStore.positionTicks = msToTicks(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.current?.pauseAsync();
|
||||
} else {
|
||||
player.current?.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;
|
@ -49,6 +49,7 @@ function postExpoEvent(event, data) {
|
||||
}));
|
||||
}
|
||||
|
||||
${StaticScriptLoader.scripts.NativeAudioPlayer}
|
||||
${StaticScriptLoader.scripts.NativeVideoPlayer}
|
||||
|
||||
${StaticScriptLoader.scripts.NativeShell}
|
||||
@ -97,15 +98,23 @@ true;
|
||||
deactivateKeepAwake();
|
||||
}
|
||||
break;
|
||||
case 'ExpoAudioPlayer.play':
|
||||
rootStore.mediaStore.type = MediaTypes.Audio;
|
||||
rootStore.mediaStore.uri = data.url;
|
||||
rootStore.mediaStore.posterUri = data.backdropUrl;
|
||||
rootStore.mediaStore.positionTicks = data.playerStartPositionTicks;
|
||||
break;
|
||||
case 'ExpoVideoPlayer.play':
|
||||
rootStore.mediaStore.type = MediaTypes.Video;
|
||||
rootStore.mediaStore.uri = data.url;
|
||||
rootStore.mediaStore.posterUri = data.backdropUrl;
|
||||
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;
|
||||
|
@ -11,6 +11,7 @@ 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';
|
||||
@ -187,6 +188,7 @@ const HomeScreen = observer(() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
/>
|
||||
<AudioPlayer/>
|
||||
<VideoPlayer/>
|
||||
</>
|
||||
) : (
|
||||
|
@ -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