Repository: expo/playlist-example Branch: master Commit: 8b91bbf0ea13 Files: 6 Total size: 25.9 KB Directory structure: gitextract_b6274l6n/ ├── .babelrc ├── .gitignore ├── App.js ├── README.md ├── app.json └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["babel-preset-expo"] } ================================================ FILE: .gitignore ================================================ node_modules/**/* .expo/* npm-debug.* ================================================ FILE: App.js ================================================ /** * @flow */ import React from "react"; import { Dimensions, Image, StyleSheet, Text, TouchableHighlight, View } from "react-native"; import { Asset } from "expo-asset"; import { Audio, InterruptionModeAndroid, InterruptionModeIOS, ResizeMode, Video } from "expo-av"; import * as Font from "expo-font"; import Slider from "@react-native-community/slider"; import { MaterialIcons } from "@expo/vector-icons"; class Icon { constructor(module, width, height) { this.module = module; this.width = width; this.height = height; Asset.fromModule(this.module).downloadAsync(); } } class PlaylistItem { constructor(name, uri, isVideo) { this.name = name; this.uri = uri; this.isVideo = isVideo; } } const PLAYLIST = [ new PlaylistItem( "Comfort Fit - “Sorry”", "https://s3.amazonaws.com/exp-us-standard/audio/playlist-example/Comfort_Fit_-_03_-_Sorry.mp3", false ), new PlaylistItem( "Big Buck Bunny", "http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4", true ), new PlaylistItem( "Mildred Bailey – “All Of Me”", "https://ia800304.us.archive.org/34/items/PaulWhitemanwithMildredBailey/PaulWhitemanwithMildredBailey-AllofMe.mp3", false ), new PlaylistItem( "Popeye - I don't scare", "https://ia800501.us.archive.org/11/items/popeye_i_dont_scare/popeye_i_dont_scare_512kb.mp4", true ), new PlaylistItem( "Podington Bear - “Rubber Robot”", "https://s3.amazonaws.com/exp-us-standard/audio/playlist-example/Podington_Bear_-_Rubber_Robot.mp3", false ) ]; const ICON_THROUGH_EARPIECE = "speaker-phone"; const ICON_THROUGH_SPEAKER = "speaker"; const ICON_PLAY_BUTTON = new Icon( require("./assets/images/play_button.png"), 34, 51 ); const ICON_PAUSE_BUTTON = new Icon( require("./assets/images/pause_button.png"), 34, 51 ); const ICON_STOP_BUTTON = new Icon( require("./assets/images/stop_button.png"), 22, 22 ); const ICON_FORWARD_BUTTON = new Icon( require("./assets/images/forward_button.png"), 33, 25 ); const ICON_BACK_BUTTON = new Icon( require("./assets/images/back_button.png"), 33, 25 ); const ICON_LOOP_ALL_BUTTON = new Icon( require("./assets/images/loop_all_button.png"), 77, 35 ); const ICON_LOOP_ONE_BUTTON = new Icon( require("./assets/images/loop_one_button.png"), 77, 35 ); const ICON_MUTED_BUTTON = new Icon( require("./assets/images/muted_button.png"), 67, 58 ); const ICON_UNMUTED_BUTTON = new Icon( require("./assets/images/unmuted_button.png"), 67, 58 ); const ICON_TRACK_1 = new Icon(require("./assets/images/track_1.png"), 166, 5); const ICON_THUMB_1 = new Icon(require("./assets/images/thumb_1.png"), 18, 19); const ICON_THUMB_2 = new Icon(require("./assets/images/thumb_2.png"), 15, 19); const LOOPING_TYPE_ALL = 0; const LOOPING_TYPE_ONE = 1; const LOOPING_TYPE_ICONS = { 0: ICON_LOOP_ALL_BUTTON, 1: ICON_LOOP_ONE_BUTTON }; const { width: DEVICE_WIDTH, height: DEVICE_HEIGHT } = Dimensions.get("window"); const BACKGROUND_COLOR = "#FFF8ED"; const DISABLED_OPACITY = 0.5; const FONT_SIZE = 14; const LOADING_STRING = "... loading ..."; const BUFFERING_STRING = "...buffering..."; const RATE_SCALE = 3.0; const VIDEO_CONTAINER_HEIGHT = (DEVICE_HEIGHT * 2.0) / 5.0 - FONT_SIZE * 2; export default class App extends React.Component { constructor(props) { super(props); this.index = 0; this.isSeeking = false; this.shouldPlayAtEndOfSeek = false; this.playbackInstance = null; this.state = { showVideo: false, playbackInstanceName: LOADING_STRING, loopingType: LOOPING_TYPE_ALL, muted: false, playbackInstancePosition: null, playbackInstanceDuration: null, shouldPlay: false, isPlaying: false, isBuffering: false, isLoading: true, fontLoaded: false, shouldCorrectPitch: true, volume: 1.0, rate: 1.0, videoWidth: DEVICE_WIDTH, videoHeight: VIDEO_CONTAINER_HEIGHT, poster: false, useNativeControls: false, fullscreen: false, throughEarpiece: false }; } componentDidMount() { Audio.setAudioModeAsync({ allowsRecordingIOS: false, staysActiveInBackground: false, interruptionModeIOS: InterruptionModeIOS.DoNotMix, playsInSilentModeIOS: true, shouldDuckAndroid: true, interruptionModeAndroid: InterruptionModeAndroid.DoNotMix, playThroughEarpieceAndroid: false }); (async () => { await Font.loadAsync({ ...MaterialIcons.font, "cutive-mono-regular": require("./assets/fonts/CutiveMono-Regular.ttf") }); this.setState({ fontLoaded: true }); })(); } async _loadNewPlaybackInstance(playing) { if (this.playbackInstance != null) { await this.playbackInstance.unloadAsync(); // this.playbackInstance.setOnPlaybackStatusUpdate(null); this.playbackInstance = null; } const source = { uri: PLAYLIST[this.index].uri }; const initialStatus = { shouldPlay: playing, rate: this.state.rate, shouldCorrectPitch: this.state.shouldCorrectPitch, volume: this.state.volume, isMuted: this.state.muted, isLooping: this.state.loopingType === LOOPING_TYPE_ONE // // UNCOMMENT THIS TO TEST THE OLD androidImplementation: // androidImplementation: 'MediaPlayer', }; if (PLAYLIST[this.index].isVideo) { await this._video.loadAsync(source, initialStatus); // this._video.onPlaybackStatusUpdate(this._onPlaybackStatusUpdate); this.playbackInstance = this._video; const status = await this._video.getStatusAsync(); } else { const { sound, status } = await Audio.Sound.createAsync( source, initialStatus, this._onPlaybackStatusUpdate ); this.playbackInstance = sound; } this._updateScreenForLoading(false); } _mountVideo = component => { this._video = component; this._loadNewPlaybackInstance(false); }; _updateScreenForLoading(isLoading) { if (isLoading) { this.setState({ showVideo: false, isPlaying: false, playbackInstanceName: LOADING_STRING, playbackInstanceDuration: null, playbackInstancePosition: null, isLoading: true }); } else { this.setState({ playbackInstanceName: PLAYLIST[this.index].name, showVideo: PLAYLIST[this.index].isVideo, isLoading: false }); } } _onPlaybackStatusUpdate = status => { if (status.isLoaded) { this.setState({ playbackInstancePosition: status.positionMillis, playbackInstanceDuration: status.durationMillis, shouldPlay: status.shouldPlay, isPlaying: status.isPlaying, isBuffering: status.isBuffering, rate: status.rate, muted: status.isMuted, volume: status.volume, loopingType: status.isLooping ? LOOPING_TYPE_ONE : LOOPING_TYPE_ALL, shouldCorrectPitch: status.shouldCorrectPitch }); if (status.didJustFinish && !status.isLooping) { this._advanceIndex(true); this._updatePlaybackInstanceForIndex(true); } } else { if (status.error) { console.log(`FATAL PLAYER ERROR: ${status.error}`); } } }; _onLoadStart = () => { console.log(`ON LOAD START`); }; _onLoad = status => { console.log(`ON LOAD : ${JSON.stringify(status)}`); }; _onError = error => { console.log(`ON ERROR : ${error}`); }; _onReadyForDisplay = event => { const widestHeight = (DEVICE_WIDTH * event.naturalSize.height) / event.naturalSize.width; if (widestHeight > VIDEO_CONTAINER_HEIGHT) { this.setState({ videoWidth: (VIDEO_CONTAINER_HEIGHT * event.naturalSize.width) / event.naturalSize.height, videoHeight: VIDEO_CONTAINER_HEIGHT }); } else { this.setState({ videoWidth: DEVICE_WIDTH, videoHeight: (DEVICE_WIDTH * event.naturalSize.height) / event.naturalSize.width }); } }; _onFullscreenUpdate = event => { console.log( `FULLSCREEN UPDATE : ${JSON.stringify(event.fullscreenUpdate)}` ); }; _advanceIndex(forward) { this.index = (this.index + (forward ? 1 : PLAYLIST.length - 1)) % PLAYLIST.length; } async _updatePlaybackInstanceForIndex(playing) { this._updateScreenForLoading(true); this.setState({ videoWidth: DEVICE_WIDTH, videoHeight: VIDEO_CONTAINER_HEIGHT }); this._loadNewPlaybackInstance(playing); } _onPlayPausePressed = () => { if (this.playbackInstance != null) { if (this.state.isPlaying) { this.playbackInstance.pauseAsync(); } else { this.playbackInstance.playAsync(); } } }; _onStopPressed = () => { if (this.playbackInstance != null) { this.playbackInstance.stopAsync(); } }; _onForwardPressed = () => { if (this.playbackInstance != null) { this._advanceIndex(true); this._updatePlaybackInstanceForIndex(this.state.shouldPlay); } }; _onBackPressed = () => { if (this.playbackInstance != null) { this._advanceIndex(false); this._updatePlaybackInstanceForIndex(this.state.shouldPlay); } }; _onMutePressed = () => { if (this.playbackInstance != null) { this.playbackInstance.setIsMutedAsync(!this.state.muted); } }; _onLoopPressed = () => { if (this.playbackInstance != null) { this.playbackInstance.setIsLoopingAsync( this.state.loopingType !== LOOPING_TYPE_ONE ); } }; _onVolumeSliderValueChange = value => { if (this.playbackInstance != null) { this.playbackInstance.setVolumeAsync(value); } }; _trySetRate = async (rate, shouldCorrectPitch) => { if (this.playbackInstance != null) { try { await this.playbackInstance.setRateAsync(rate, shouldCorrectPitch); } catch (error) { // Rate changing could not be performed, possibly because the client's Android API is too old. } } }; _onRateSliderSlidingComplete = async value => { this._trySetRate(value * RATE_SCALE, this.state.shouldCorrectPitch); }; _onPitchCorrectionPressed = async value => { this._trySetRate(this.state.rate, !this.state.shouldCorrectPitch); }; _onSeekSliderValueChange = value => { if (this.playbackInstance != null && !this.isSeeking) { this.isSeeking = true; this.shouldPlayAtEndOfSeek = this.state.shouldPlay; this.playbackInstance.pauseAsync(); } }; _onSeekSliderSlidingComplete = async value => { if (this.playbackInstance != null) { this.isSeeking = false; const seekPosition = value * this.state.playbackInstanceDuration; if (this.shouldPlayAtEndOfSeek) { this.playbackInstance.playFromPositionAsync(seekPosition); } else { this.playbackInstance.setPositionAsync(seekPosition); } } }; _getSeekSliderPosition() { if ( this.playbackInstance != null && this.state.playbackInstancePosition != null && this.state.playbackInstanceDuration != null ) { return ( this.state.playbackInstancePosition / this.state.playbackInstanceDuration ); } return 0; } _getMMSSFromMillis(millis) { const totalSeconds = millis / 1000; const seconds = Math.floor(totalSeconds % 60); const minutes = Math.floor(totalSeconds / 60); const padWithZero = number => { const string = number.toString(); if (number < 10) { return "0" + string; } return string; }; return padWithZero(minutes) + ":" + padWithZero(seconds); } _getTimestamp() { if ( this.playbackInstance != null && this.state.playbackInstancePosition != null && this.state.playbackInstanceDuration != null ) { return `${this._getMMSSFromMillis( this.state.playbackInstancePosition )} / ${this._getMMSSFromMillis(this.state.playbackInstanceDuration)}`; } return ""; } _onPosterPressed = () => { this.setState({ poster: !this.state.poster }); }; _onUseNativeControlsPressed = () => { this.setState({ useNativeControls: !this.state.useNativeControls }); }; _onFullscreenPressed = () => { try { this._video.presentFullscreenPlayer(); } catch (error) { console.log(error.toString()); } }; _onSpeakerPressed = () => { this.setState( state => { return { throughEarpiece: !state.throughEarpiece }; }, () => Audio.setAudioModeAsync({ allowsRecordingIOS: false, interruptionModeIOS: InterruptionModeIOS.DoNotMix, playsInSilentModeIOS: true, shouldDuckAndroid: true, interruptionModeAndroid: InterruptionModeAndroid.DoNotMix, playThroughEarpieceAndroid: this.state.throughEarpiece }) ); }; render() { return !this.state.fontLoaded ? ( ) : ( {this.state.playbackInstanceName} {this.state.isBuffering ? BUFFERING_STRING : ""} {this._getTimestamp()} this._trySetRate(1.0, this.state.shouldCorrectPitch)} > Rate: PC: {this.state.shouldCorrectPitch ? "yes" : "no"} {this.state.showVideo ? ( Poster: {this.state.poster ? "yes" : "no"} Fullscreen Native Controls:{" "} {this.state.useNativeControls ? "yes" : "no"} ) : null} ); } } const styles = StyleSheet.create({ emptyContainer: { alignSelf: "stretch", backgroundColor: BACKGROUND_COLOR }, container: { flex: 1, flexDirection: "column", justifyContent: "space-between", alignItems: "center", alignSelf: "stretch", backgroundColor: BACKGROUND_COLOR }, wrapper: {}, nameContainer: { height: FONT_SIZE }, space: { height: FONT_SIZE }, videoContainer: { height: VIDEO_CONTAINER_HEIGHT }, video: { maxWidth: DEVICE_WIDTH }, playbackContainer: { flex: 1, flexDirection: "column", justifyContent: "space-between", alignItems: "center", alignSelf: "stretch", minHeight: ICON_THUMB_1.height * 2.0, maxHeight: ICON_THUMB_1.height * 2.0 }, playbackSlider: { alignSelf: "stretch" }, timestampRow: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "space-between", alignSelf: "stretch", minHeight: FONT_SIZE }, text: { fontSize: FONT_SIZE, minHeight: FONT_SIZE }, buffering: { textAlign: "left", paddingLeft: 20 }, timestamp: { textAlign: "right", paddingRight: 20 }, button: { backgroundColor: BACKGROUND_COLOR }, buttonsContainerBase: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "space-between" }, buttonsContainerTopRow: { maxHeight: ICON_PLAY_BUTTON.height, minWidth: DEVICE_WIDTH / 2.0, maxWidth: DEVICE_WIDTH / 2.0 }, buttonsContainerMiddleRow: { maxHeight: ICON_MUTED_BUTTON.height, alignSelf: "stretch", paddingRight: 20 }, volumeContainer: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "space-between", minWidth: DEVICE_WIDTH / 2.0, maxWidth: DEVICE_WIDTH / 2.0 }, volumeSlider: { width: DEVICE_WIDTH / 2.0 - ICON_MUTED_BUTTON.width }, buttonsContainerBottomRow: { maxHeight: 32, alignSelf: "stretch", paddingRight: 20, paddingLeft: 20 }, rateSlider: { width: DEVICE_WIDTH / 2.0 }, buttonsContainerTextRow: { maxHeight: FONT_SIZE, alignItems: "center", paddingRight: 20, paddingLeft: 20, minWidth: DEVICE_WIDTH, maxWidth: DEVICE_WIDTH } }); ================================================ FILE: README.md ================================================ # playlist example An example app using the [Expo.Audio](https://docs.expo.io/versions/latest/sdk/audio/) & [Expo.Video](https://docs.expo.io/versions/latest/sdk/video/) API. See [App.js](https://github.com/expo/playlist-example/blob/master/App.js) for the good stuff. --- ### Please report any issues at the [main Expo repository](https://github.com/expo/expo/issues) ================================================ FILE: app.json ================================================ { "expo": { "name": "playlist-example", "description": "a playlist of some songs that Greg likes to demo the Expo AV API.", "scheme": "playlist-example", "slug": "playlist", "privacy": "public", "version": "3.0.0", "platforms": ["android", "ios"], "githubUrl": "https://github.com/expo/playlist-example", "orientation": "portrait", "primaryColor": "#cccccc", "icon": "./assets/images/icon.png", "userInterfaceStyle": "light", "splash": { "backgroundColor": "#ffffff", "image": "./assets/images/icon.png", "resizeMode": "contain" }, "packagerOpts": { "assetExts": ["png", "ttf"] }, "ios": { "supportsTablet": true }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/images/icon.png", "backgroundColor": "#ffffff" } }, "plugins": [ "expo-asset", "expo-font" ] } } ================================================ FILE: package.json ================================================ { "name": "playlist-example", "version": "3.0.0", "description": "a playlist of some songs that Greg likes to demo the Expo AV API.", "author": "expo", "keywords": [ "expo", "react-native", "audio", "playlist" ], "private": true, "main": "node_modules/expo/AppEntry.js", "dependencies": { "@react-native-community/slider": "4.5.2", "expo": "~51.0.14", "expo-asset": "~10.0.9", "expo-av": "~14.0.5", "expo-font": "~12.0.7", "expo-updates": "~0.25.17", "react": "18.2.0", "react-native": "0.74.2" }, "devDependencies": { "@babel/core": "^7.24.0" } }