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