Merge pull request #339 from thornbill/native-audio

Add experimental native audio player
This commit is contained in:
Bill Thornton 2022-04-06 09:56:37 -04:00 committed by GitHub
commit cf20a46163
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 636 additions and 26 deletions

View File

@ -119,6 +119,7 @@ window.NativeShell = {
return [];
}
return [
'NativeAudioPlayer',
'NativeVideoPlayer'
];
},

View 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
View 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;

View File

@ -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;

View File

@ -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}

View 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();
});
});

View File

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AudioPlayer should render correctly 1`] = `null`;

View File

@ -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;

View File

@ -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
}
});

View File

@ -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/>
</>
) : (

View File

@ -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>
`;

View File

@ -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

View File

@ -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
});

View File

@ -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);
});

View File

@ -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'));