0

I'm trying convert "expo-react-native audio player and video example" to funtional component. This's code base:

expo/playlist-example

and this's my code that I'm trying convert to funtional component:

import React, { useState } 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";
import { PLAYLIST } from "../data/PlayList";
import { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from "react/cjs/react.development";

const ICON_STOP_BUTTON = () => (<Image source={(require("../assets/images/stop_button.png"))} style={{ width: 22, height: 22 }} />);
const ICON_FORWARD_BUTTON = () => (<Image source={(require("../assets/images/forward_button.png"))} style={{ width: 33, height: 25 }} />);
const ICON_BACK_BUTTON = () => (<Image source={(require("../assets/images/back_button.png"))} style={{ width: 33, height: 25 }} />);

const ICON_LOOP_ALL_BUTTON = require("../assets/images/loop_all_button.png")
const ICON_LOOP_ONE_BUTTON = require("../assets/images/loop_one_button.png")

const ICON_THROUGH_EARPIECE = "speaker-phone";
const ICON_THROUGH_SPEAKER = "speaker";


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 function MusicPlayerScreen() {

  const [index, setIndex] = useState(0)
  const [isSeeking, setIsSeeking] = useState(false)
  const [shouldPlayAtEndOfSeek, setShouldPlayAtEndOfSeek] = useState(false)
  const [playbackInstance, setPlaybackInstance] = useState(null)

  const [state, setState] = useState({
    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")
      });
      setState({ fontLoaded: true });
    })();
  }

  async function _loadNewPlaybackInstance(playing) {
    if (playbackInstance != null) {
      await playbackInstance.unloadAsync();
      // playbackInstance.setOnPlaybackStatusUpdate(null);
      setPlaybackInstance(null);
    }

    const source = { uri: PLAYLIST[index].uri };
    const [initialStatus, setInitialStatus] = useState({
      shouldPlay: playing,
      rate: state.rate,
      shouldCorrectPitch: state.shouldCorrectPitch,
      volume: state.volume,
      isMuted: state.muted,
      isLooping: state.loopingType === LOOPING_TYPE_ONE
      // // UNCOMMENT THIS TO TEST THE OLD androidImplementation:
      // androidImplementation: 'MediaPlayer',
    });

    if (PLAYLIST[index].isVideo) {
      await _video.loadAsync(source, initialStatus);
      // _video.onPlaybackStatusUpdate(_onPlaybackStatusUpdate);
      setPlaybackInstance = _video;
      const status = await _video.getStatusAsync();
    } else {
      const { sound, status } = await Audio.Sound.createAsync(
        source,
        initialStatus,
        _onPlaybackStatusUpdate
      );
      setPlaybackInstance = sound;
    }

    _updateScreenForLoading(false);
  }

  _mountVideo = component => {
    _video = component;
    _loadNewPlaybackInstance(false);
  };

  _updateScreenForLoading = (isLoading) => {
    if (isLoading) {
      setState({
        showVideo: false,
        isPlaying: false,
        playbackInstanceName: LOADING_STRING,
        playbackInstanceDuration: null,
        playbackInstancePosition: null,
        isLoading: true
      });
    } else {
      setState({
        playbackInstanceName: PLAYLIST[index].name,
        showVideo: PLAYLIST[index].isVideo,
        isLoading: false
      });
    }
  };

  _onPlaybackStatusUpdate = status => {
    if (status.isLoaded) {
      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) {
        _advanceIndex(true);
        _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) {
      setState({
        videoWidth:
          (VIDEO_CONTAINER_HEIGHT * event.naturalSize.width) /
          event.naturalSize.height,
        videoHeight: VIDEO_CONTAINER_HEIGHT
      });
    } else {
      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) => {
    setIndex((index + (forward ? 1 : PLAYLIST.length - 1)) % PLAYLIST.length);
  }

  async function _updatePlaybackInstanceForIndex(playing) {
    _updateScreenForLoading(true);

    setState({
      videoWidth: DEVICE_WIDTH,
      videoHeight: VIDEO_CONTAINER_HEIGHT
    });

    _loadNewPlaybackInstance(playing);
  }

  _onPlayPausePressed = () => {
    if (playbackInstance != null) {
      if (state.isPlaying) {
        playbackInstance.pauseAsync();
      } else {
        playbackInstance.playAsync();
      }
    }
  };

  _onStopPressed = () => {
    if (playbackInstance != null) {
      playbackInstance.stopAsync();
    }
  };

  _onForwardPressed = () => {
    if (playbackInstance != null) {
      _advanceIndex(true);
      _updatePlaybackInstanceForIndex(state.shouldPlay);
    }
  };

  _onBackPressed = () => {
    if (playbackInstance != null) {
      _advanceIndex(false);
      _updatePlaybackInstanceForIndex(state.shouldPlay);
    }
  };

  _onMutePressed = () => {
    if (playbackInstance != null) {
      playbackInstance.setIsMutedAsync(!state.muted);
    }
  };

  _onLoopPressed = () => {
    if (playbackInstance != null) {
      playbackInstance.setIsLoopingAsync(
        state.loopingType !== LOOPING_TYPE_ONE
      );
    }
  };

  _onVolumeSliderValueChange = value => {
    if (playbackInstance != null) {
      playbackInstance.setVolumeAsync(value);
    }
  };

  _trySetRate = async (rate, shouldCorrectPitch) => {
    if (playbackInstance != null) {
      try {
        await 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 => {
    _trySetRate(value * RATE_SCALE, state.shouldCorrectPitch);
  };

  _onPitchCorrectionPressed = async value => {
    _trySetRate(state.rate, !state.shouldCorrectPitch);
  };

  _onSeekSliderValueChange = value => {
    if (playbackInstance != null && !isSeeking) {
      setIsSeeking(true);
      shouldPlayAtEndOfSeek = state.shouldPlay;
      playbackInstance.pauseAsync();
    }
  };

  _onSeekSliderSlidingComplete = async value => {
    if (playbackInstance != null) {
      setIsSeeking(false);
      const seekPosition = value * state.playbackInstanceDuration;
      if (shouldPlayAtEndOfSeek) {
        playbackInstance.playFromPositionAsync(seekPosition);
      } else {
        playbackInstance.setPositionAsync(seekPosition);
      }
    }
  };

  _getSeekSliderPosition = () => {
    if (
      playbackInstance != null &&
      state.playbackInstancePosition != null &&
      state.playbackInstanceDuration != null
    ) {
      return (
        state.playbackInstancePosition /
        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 (
      playbackInstance != null &&
      state.playbackInstancePosition != null &&
      state.playbackInstanceDuration != null
    ) {
      return `${_getMMSSFromMillis(
        state.playbackInstancePosition
      )} / ${_getMMSSFromMillis(state.playbackInstanceDuration)}`;
    }
    return "";
  }

  _onPosterPressed = () => {
    setState({ poster: !state.poster });
  };

  _onUseNativeControlsPressed = () => {
    setState({ useNativeControls: !state.useNativeControls });
  };

  _onFullscreenPressed = () => {
    try {
      _video.presentFullscreenPlayer();
    } catch (error) {
      console.log(error.toString());
    }
  };

  _onSpeakerPressed = () => {
    setState(
      state => {
        return { throughEarpiece: !state.throughEarpiece };
      },
      () =>
        Audio.setAudioModeAsync({
          allowsRecordingIOS: false,
          interruptionModeIOS: InterruptionModeIOS.DoNotMix,
          playsInSilentModeIOS: true,
          shouldDuckAndroid: true,
          interruptionModeAndroid: InterruptionModeAndroid.DoNotMix,
          playThroughEarpieceAndroid: state.throughEarpiece
        })
    );
  };

  return !state.fontLoaded ? (
    <View style={styles.emptyContainer} />
  ) : (
    <View style={styles.container}>
      <View />
      <View style={styles.nameContainer}>
        <Text style={[styles.text, { fontFamily: "cutive-mono-regular" }]}>
          {state.playbackInstanceName}
        </Text>
      </View>
      <View style={styles.space} />
      <View style={styles.videoContainer}>
        <Video
          ref={_mountVideo}
          style={[
            styles.video,
            {
              opacity: state.showVideo ? 1.0 : 0.0,
              width: state.videoWidth,
              height: state.videoHeight
            }
          ]}
          resizeMode={ResizeMode.CONTAIN}
          onPlaybackStatusUpdate={_onPlaybackStatusUpdate}
          onLoadStart={_onLoadStart}
          onLoad={_onLoad}
          onError={_onError}
          onFullscreenUpdate={_onFullscreenUpdate}
          onReadyForDisplay={_onReadyForDisplay}
          useNativeControls={state.useNativeControls}
        />
      </View>
      <View
        style={[
          styles.playbackContainer,
          {
            opacity: state.isLoading ? DISABLED_OPACITY : 1.0
          }
        ]}
      >
        <Slider
          style={styles.playbackSlider}
          trackImage={() => { require("../assets/images/track_1.png") }}
          thumbImage={() => { require("../assets/images/thumb_1.png") }}
          value={_getSeekSliderPosition()}
          onValueChange={_onSeekSliderValueChange}
          onSlidingComplete={_onSeekSliderSlidingComplete}
          disabled={state.isLoading}
        />
        <View style={styles.timestampRow}>
          <Text
            style={[
              styles.text,
              styles.buffering,
              { fontFamily: "cutive-mono-regular" }
            ]}
          >
            {state.isBuffering ? BUFFERING_STRING : ""}
          </Text>
          <Text
            style={[
              styles.text,
              styles.timestamp,
              { fontFamily: "cutive-mono-regular" }
            ]}
          >
            {_getTimestamp()}
          </Text>
        </View>
      </View>
      <View
        style={[
          styles.buttonsContainerBase,
          styles.buttonsContainerTopRow,
          {
            opacity: state.isLoading ? DISABLED_OPACITY : 1.0
          }
        ]}
      >
        <TouchableHighlight
          underlayColor={BACKGROUND_COLOR}
          style={styles.wrapper}
          onPress={_onBackPressed}
          disabled={state.isLoading}
        >
          {ICON_BACK_BUTTON}
        </TouchableHighlight>
        <TouchableHighlight
          underlayColor={BACKGROUND_COLOR}
          style={styles.wrapper}
          onPress={_onPlayPausePressed}
          disabled={state.isLoading}
        >
          <Image
            style={styles.button}
            source={require(
              state.isPlaying
                ? "../assets/images/pause_button.png"
                : "../assets/images/play_button.png"
            )}
          />
        </TouchableHighlight>
        <TouchableHighlight
          underlayColor={BACKGROUND_COLOR}
          style={styles.wrapper}
          onPress={_onStopPressed}
          disabled={state.isLoading}
        >
          {ICON_STOP_BUTTON}
        </TouchableHighlight>
        <TouchableHighlight
          underlayColor={BACKGROUND_COLOR}
          style={styles.wrapper}
          onPress={_onForwardPressed}
          disabled={state.isLoading}
        >
          {ICON_FORWARD_BUTTON}
        </TouchableHighlight>
      </View>
      <View
        style={[
          styles.buttonsContainerBase,
          styles.buttonsContainerMiddleRow
        ]}
      >
        <View style={styles.volumeContainer}>
          <TouchableHighlight
            underlayColor={BACKGROUND_COLOR}
            style={styles.wrapper}
            onPress={_onMutePressed}
          >
            <Image
              style={styles.button}
              source={require(
                state.muted
                  ? "../assets/images/muted_button.png"
                  : "../assets/images/unmuted_button.png"
              )}
            />
          </TouchableHighlight>
          <Slider
            style={styles.volumeSlider}
            trackImage={() => { require("../assets/images/track_1.png") }}
            thumbImage={() => { require("../assets/images/thumb_2.png") }}
            value={1}
            onValueChange={_onVolumeSliderValueChange}
          />
        </View>
        <TouchableHighlight
          underlayColor={BACKGROUND_COLOR}
          style={styles.wrapper}
          onPress={_onLoopPressed}
        >
          <Image
            style={styles.button}
            source={LOOPING_TYPE_ICONS[state.loopingType]}
          />
        </TouchableHighlight>
      </View>
      <View
        style={[
          styles.buttonsContainerBase,
          styles.buttonsContainerBottomRow
        ]}
      >
        <TouchableHighlight
          underlayColor={BACKGROUND_COLOR}
          style={styles.wrapper}
          onPress={() => _trySetRate(1.0, state.shouldCorrectPitch)}
        >
          <View style={styles.button}>
            <Text
              style={[styles.text, { fontFamily: "cutive-mono-regular" }]}
            >
              Rate:
            </Text>
          </View>
        </TouchableHighlight>
        <Slider
          style={styles.rateSlider}
          trackImage={() => { require("../assets/images/track_1.png") }}
          thumbImage={() => { require("../assets/images/thumb_1.png") }}
          value={state.rate / RATE_SCALE}
          onSlidingComplete={_onRateSliderSlidingComplete}
        />
        <TouchableHighlight
          underlayColor={BACKGROUND_COLOR}
          style={styles.wrapper}
          onPress={_onPitchCorrectionPressed}
        >
          <View style={styles.button}>
            <Text
              style={[styles.text, { fontFamily: "cutive-mono-regular" }]}
            >
              PC: {state.shouldCorrectPitch ? "yes" : "no"}
            </Text>
          </View>
        </TouchableHighlight>
        <TouchableHighlight
          onPress={_onSpeakerPressed}
          underlayColor={BACKGROUND_COLOR}
        >
          <MaterialIcons
            name={
              state.throughEarpiece
                ? ICON_THROUGH_EARPIECE
                : ICON_THROUGH_SPEAKER
            }
            size={32}
            color="black"
          />
        </TouchableHighlight>
      </View>
      <View />
      {state.showVideo ? (
        <View>
          <View
            style={[
              styles.buttonsContainerBase,
              styles.buttonsContainerTextRow
            ]}
          >
            <View />
            <TouchableHighlight
              underlayColor={BACKGROUND_COLOR}
              style={styles.wrapper}
              onPress={_onPosterPressed}
            >
              <View style={styles.button}>
                <Text
                  style={[styles.text, { fontFamily: "cutive-mono-regular" }]}
                >
                  Poster: {state.poster ? "yes" : "no"}
                </Text>
              </View>
            </TouchableHighlight>
            <View />
            <TouchableHighlight
              underlayColor={BACKGROUND_COLOR}
              style={styles.wrapper}
              onPress={_onFullscreenPressed}
            >
              <View style={styles.button}>
                <Text
                  style={[styles.text, { fontFamily: "cutive-mono-regular" }]}
                >
                  Fullscreen
                </Text>
              </View>
            </TouchableHighlight>
            <View />
          </View>
          <View style={styles.space} />
          <View
            style={[
              styles.buttonsContainerBase,
              styles.buttonsContainerTextRow
            ]}
          >
            <View />
            <TouchableHighlight
              underlayColor={BACKGROUND_COLOR}
              style={styles.wrapper}
              onPress={_onUseNativeControlsPressed}
            >
              <View style={styles.button}>
                <Text
                  style={[styles.text, { fontFamily: "cutive-mono-regular" }]}
                >
                  Native Controls:{" "}
                  {state.useNativeControls ? "yes" : "no"}
                </Text>
              </View>
            </TouchableHighlight>
            <View />
          </View>
        </View>
      ) : null}
    </View>
  );
}

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: 18 * 2.0,
    maxHeight: 19 * 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: 51,
    minWidth: DEVICE_WIDTH / 2.0,
    maxWidth: DEVICE_WIDTH / 2.0
  },
  buttonsContainerMiddleRow: {
    maxHeight: 58,
    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 - 67
  },
  buttonsContainerBottomRow: {
    maxHeight: 19,
    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
  }
});

I was searched all on google, github, and other questions in stackoverflow but I still haven't found the desired answer. I'm a newbie hoping someone can help me!

0 Answers0