React Native視頻播放方案

做過RN的童鞋都知道,RN上官方的視頻組件是react-native-video。然而,官方的文檔的demo並不是那麼詳盡,踩了一身的坑,仍然和理想中的視頻播放器相去甚遠。本文會完成一個基本的視頻播放器,包含:

  • 全屏切換
  • 播放/暫停
  • 進度拖動
  • 滑動手勢控制音量、亮度、進度

完整例子見文末。

全屏方案

一般而言,思路有兩種。一種是用戶點擊全屏按鈕時,另外打開一個頁面,該頁面全屏展示一個視頻組件,並且應用轉爲橫屏;二是將當前頁面上的小視頻組件通過一系列CSS變換,鋪滿屏幕,並且將應用轉爲橫屏。本人兩種思路都實現過。
第一種方案需要考慮小視頻組件和全屏視頻組件兩個組件的進度同步性,並且如果頁面上存在多個視頻組件,各組件之間互不干擾,如下:

小視頻組件1 <----> 全屏組件1
小視頻組件2 <----> 全屏組件2
小視頻組件3 <----> 全屏組件3

看起來我們將小窗口和全屏分爲了兩個組件,只需要關心同步問題,實際實現上,還是有若干碰壁的。其最致命的問題是,無論如何同步,切換全屏和切回時,都會有一定的不連貫性,甚至有倒幀的現象。

第二種思路,通過CSS變換,將小視頻的窗口全屏鋪滿,這首先避免了同步問題,切換全屏的時候也如絲般順滑。然而,第二種思路有個大問題是,小視頻在佈局中的位置是不確定的。在複雜佈局中,有可能嵌套的很深,並且不在頁面頭部的位置。加上RN裏沒有fix佈局,通過CSS鋪滿全屏異常困難。因此,第二種思路要求,無論視頻組件嵌套多深,該組件必須位於頁面頂部,這樣方便CSS計算。

本文采用第二種思路,視頻組件所有用到的三方應用如下:

import React from 'react';
import {
  AppState,
  AsyncStorage,
  BackHandler,
  Dimensions,
  Image,
  Platform,
  Slider,
  StyleSheet,
  Text,
  TouchableOpacity,
  View
} from 'react-native';
import SystemSetting from 'react-native-system-setting'  // 一個系統亮度、音量控制相關的庫,npm安裝
import LinearGradient from 'react-native-linear-gradient';  // 一個漸變色庫,用於美化,npm安裝
import {rpx} from "../util/AdapterUtil";  // 一個將設計尺寸轉換爲dp單位的工具方法,具體實現見下
import Orientation from 'react-native-orientation'  // 一個控制應用方向的庫,npm安裝
import Video from "react-native-video";  // RN視頻組件,npm安裝。注意不同版本的庫,其bug和兼容性問題相差巨大,本文用的是4.4.2版本
import {log} from "../util/artUtils";  // 一個log工具類,可無視

rpx方法實現如下,我相信很多應用都有大同小異的實現,該方法將設計尺寸寬設定爲750,也就是rpx(750)爲屏幕寬度:

const width = Dimensions.get('window').width;
const designWidth = 750
export function rpx (num) {
  return num * width / designWidth
}

注意,嵌套在視頻組件外面的任何父組件不能指定寬度:

<View style={{width: rpx(750)}}>
	<MyVideo></MyVideo>
</View>

如上是有問題的,因爲應用轉爲橫屏後,組件寬度沒變,導致父組件橫向沒有鋪滿屏幕,而視頻組件鋪滿了,超出了父組件。又因爲TouchableOpacity在Android上,存在“超出父組件部分無法點擊”的bug,超出父組件的按鈕就無法響應了。

現在,如果是小視頻,可以通過props指定style控制窗口大小;而如果是全屏,則簡單的鋪滿全屏:

  formatFullScreenContainer() {
    const w = Dimensions.get('window').width;
    const h = Dimensions.get('window').height;
    return {
      position: 'absolute',
      left: 0,
      top: 0,
      width: this.isL ? w : h,
      height: this.isL ? h : w,
      zIndex: 99999,
      backgroundColor: '#000'
    }
  }

this.isL單純是一個記錄橫豎屏狀態的變量。現在寫一個方法切換全屏:

  toggleFullScreen() {
    if (this.player) {
      let {fullScreen} = this.state;
      if (this.props.onFullScreenChange) {
        this.props.onFullScreenChange(!fullScreen)
      }
      if (fullScreen) {
        // if (Platform.OS === 'android') {
        //   this.player.dismissFullscreenPlayer();
        // }
        this.isL = false;
        Orientation.lockToPortrait()
      } else {
        if (Platform.OS === 'android') {
          this.player.presentFullscreenPlayer();
        }
        this.isL = true;
        Orientation.lockToLandscape()
      }
      clearTimeout(this.touchTimer);
      this.setState({fullScreen: !fullScreen, isTouchedScreen: false})
    }
  }

render部分的核心代碼如下:

  render() {
    let {url, videoStyle, title} = this.props;
    let {paused, fullScreen, isTouchedScreen, duration, showVolumeSlider, showBrightnessSlider, showTimeSlider} = this.state;
    let {volume, brightness, tempCurrentTime} = this.state;
    const w = fullScreen ? Dimensions.get('window').width : videoStyle.width;
    let containerStyle = this.formatVideoStyle(videoStyle);
    return (
      <View style={fullScreen ? this.formatFullScreenContainer() : containerStyle}
            activeOpacity={1}>
        <Video source={{uri: url}} //require('../img/English_sing.mp3')
               ref={ref => this.player = ref}
               rate={1}
               volume={1.0}
               muted={false}
               paused={paused}
               resizeMode="contain"
               repeat={false}
               playInBackground={false}
               playWhenInactive={false}
               ignoreSilentSwitch={"ignore"}
               progressUpdateInterval={250.0}
               style={[fullScreen ? this.formatFullScreenContainer() : styles.videoPlayer]}
               onSeek={() => {
               }}
               onLoad={data => this.onLoad(data)}
               onError={(data) => this.onError(data)}
               onProgress={(data) => this.setTime(data)}
               onEnd={(data) => this.onEnd(data)}
        />
		...

進度拖動

可以用Slider組件,用於控制視頻進度。render中核心代碼如下:

<Slider
   style={styles.slider}
   value={this.state.slideValue}
   maximumValue={this.state.duration}
   minimumTrackTintColor={'#ca3839'}
   maximumTrackTintColor={'#989898'}
   thumbImage={require('../img/slider.png')}
   step={1}
   onValueChange={value => this.onSeek(value)}
   onSlidingComplete={value => {
     this.player.seek(value);
     this.setState({enableSetTime: true})
   }}
 />

拖動控制音量、亮度、進度

這種需求也是很多的。很多視頻app在視頻左邊上下滑動控制亮度,右邊上下滑動控制亮度,而左右滑動控制進度。我們可以在View上註冊onResponderStart、onResponderMove、onResponderEnd等事件處理滑動,判斷滑動方向和距離來做出相應控制。這裏一個細節是滑動和其他點擊組件的衝突處理;二是“過滑動折返”問題。“過滑動折返”指,你設定了用戶向上滑動400px時,能將音量從0加到100%。而用戶手快劃了600px,這種情況下,其實劃了400px音量已經是最大了,剩下200px保持最大音量不變;到最高點時向下折返,用戶不需要額外滑動200px,只需要下滑400px就可以將音量調整爲0,而不是下滑600px。也就是變向的時候,我們需要做處理。

核心處理代碼由若干監聽組成:

this.gestureHandlers = {

      onStartShouldSetResponder: (evt) => {
        log('onStartShouldSetResponder');
        return true;
      },

      onMoveShouldSetResponder: (evt) => {
        log('onMoveShouldSetResponder');
        return !this.state.paused;
      },

      onResponderGrant: (evt) => {
        log('onResponderGrant');
      },

      onResponderReject: (evt) => {
        log('onResponderReject');
      },

      onResponderStart: (evt) => {
        log('onResponderStart', evt.nativeEvent.locationX, evt.nativeEvent.locationY);
        this.touchAction.x = evt.nativeEvent.locationX;
        this.touchAction.y = evt.nativeEvent.locationY;
        SystemSetting.getVolume().then((volume) => {
          SystemSetting.getAppBrightness().then((brightness) => {
            this.setState({
              initVolume: volume,
              initBrightness: brightness,
              initTime: this.state.currentTime / this.state.duration,
              tempCurrentTime: this.state.currentTime / this.state.duration,
              showVolumeSlider: false,
              showBrightnessSlider: false,
              showTimeSlider: false
            });
            if (this.sliderTimer) {
              clearTimeout(this.sliderTimer);
              this.sliderTimer = null;
            }
          });
        });
      },

      onResponderMove: (evt) => {
        let formatValue = (v) => {
          if (v > 1.0) return {v: 1.0, outOfRange: true};
          if (v < 0) return {v: 0, outOfRange: true};
          return {v, outOfRange: false}
        };

        let resolveOutOfRange = (outOfRange, props, onSlideBack) => {
          if (outOfRange) {
            if (this.touchAction[props.limit] !== null
              && Math.abs(this.touchAction[props.v] - props.current) < Math.abs(this.touchAction[props.v] - this.touchAction[props.limit])) {
              log('outOfRange', this.touchAction[props.v], props.current, this.touchAction[props.limit]);
              // 用戶把亮度、音量調到極限(0或1.0)後,繼續滑動,並且反向滑動,此時重新定義基準落點
              this.touchAction[props.v] = this.touchAction[props.limit];
              this.touchAction[props.limit] = null;
              if (onSlideBack) onSlideBack()
            } else {
              this.touchAction[props.limit] = [props.current];
            }
          }
        };

        let currentX = evt.nativeEvent.locationX;
        let currentY = evt.nativeEvent.locationY;
        let {fullScreen} = this.state;
        let {videoStyle} = this.props;
        let w = fullScreen ? Dimensions.get('window').width : videoStyle.width;
        let h = fullScreen ? Dimensions.get('window').height : videoStyle.height;

        if (!this.touchAction.slideDirection) {
          if (Math.abs(currentY - this.touchAction.y) > rpx(50)) {
            this.touchAction.slideDirection = 'v'
          } else if (Math.abs(currentX - this.touchAction.x) > rpx(50)) {
            this.touchAction.slideDirection = 'h'
          }
        }

        if (this.touchAction.slideDirection === 'v') {
          log('onResponderMove');
          this.touchAction.isClick = false;
          let dy = this.touchAction.y - currentY;
          let dv = dy / (h / 1.5);
          if (this.touchAction.x > w / 2) {
            let {v, outOfRange} = formatValue(this.state.initVolume + dv);
            log('volume', v, outOfRange);
            this.setState({volume: v, showVolumeSlider: true});
            SystemSetting.setVolume(v);
            resolveOutOfRange(outOfRange, {current: currentY, v: 'y', limit: 'limitY'}, () => {
              this.setState({initVolume: v})
            })
          } else {
            let {v, outOfRange} = formatValue(this.state.initBrightness + dv);
            log('brightness', v, outOfRange);
            this.setState({brightness: v, showBrightnessSlider: true});
            SystemSetting.setAppBrightness(v);
            resolveOutOfRange(outOfRange, {current: currentY, v: 'y', limit: 'limitY'}, () => {
              this.setState({initBrightness: v})
            })
          }
        } else if (this.touchAction.slideDirection === 'h') {
          log('onResponderMove');
          this.touchAction.isClick = false;
          let dx = currentX - this.touchAction.x;
          let dv = dx / (w / 2);
          let {v, outOfRange} = formatValue(this.state.initTime + dv);
          log('time', v, outOfRange);
          this.setState({tempCurrentTime: v, showTimeSlider: true});
          resolveOutOfRange(outOfRange, {current: currentX, v: 'x', limit: 'limitX'}, () => {
            this.setState({initTime: v})
          })
        }
      },

      onResponderRelease: (evt) => {
        log('onResponderRelease');
      },

      onResponderEnd: (evt) => {
        let currentX = evt.nativeEvent.locationX;
        let currentY = evt.nativeEvent.locationY;
        let {fullScreen, tempCurrentTime, duration} = this.state;
        if (Math.abs(currentX - this.touchAction.x) <= rpx(50) && this.touchAction.isClick) {
          // 觸發點擊
          if (this.state.paused) {
            this.play()
          } else {
            this.onTouchedScreen()
          }
        }
        if (!this.touchAction.isClick) {
          this.setState({initVolume: this.state.volume, initBrightness: this.state.brightness});
        }
        this.sliderTimer = setTimeout(() => {
          this.sliderTimer = null;
          this.setState({showVolumeSlider: false, showBrightnessSlider: false, tempCurrentTime: null})
        }, 1000);
        if (tempCurrentTime != null) {
          let time = Math.ceil(tempCurrentTime * duration);
          this.onSeek(time);
          this.player.seek(time);
          this.setState({enableSetTime: true, showTimeSlider: false})
        } else {
          this.setState({showTimeSlider: false})
        }
        this.touchAction = {
          x: null,
          y: null,
          slideDirection: null,
          limitX: null,
          limitY: null,
          isClick: true
        };
      },

      onResponderTerminationRequest: (evt) => {
        log('onResponderTerminationRequest');
        return true;
      },

      onResponderTerminate: (evt) => {
        log('onResponderTerminate');
      }
    }

完整代碼

import React from 'react';
import {
  AppState,
  AsyncStorage,
  BackHandler,
  Dimensions,
  Image,
  Platform,
  Slider,
  StyleSheet,
  Text,
  TouchableOpacity,
  View
} from 'react-native';
import SystemSetting from 'react-native-system-setting'
import LinearGradient from 'react-native-linear-gradient';
import {rpx} from "../util/AdapterUtil";
import Orientation from 'react-native-orientation'
import Video from "react-native-video";
import {log} from "../util/artUtils";

export default class PlayerV2 extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      fullScreen: false,
      initVolume: 1.0,
      volume: 1.0,
      initBrightness: 1.0,
      brightness: 1.0,
      initTime: 0,
      tempCurrentTime: null,
      slideValue: 0.00,
      currentTime: 0.00,
      duration: 0.00,
      enableSetTime: true,
      paused: true,
      playerLock: false,
      canShowCover: false,
      isTouchedScreen: false,
      showVolumeSlider: false,
      showBrightnessSlider: false,
      showTimeSlider: false,
    };
    this.touchTimer = null;
    this.sliderTimer = null;
    this.isL = false;

    SystemSetting.getVolume().then((volume) => {
      SystemSetting.getBrightness().then((brightness) => {
        this.setState({volume, brightness, initVolume: volume, initBrightness: brightness})
      });
    });

    this.backListener = () => {
      if (this.state.fullScreen) {
        this.toggleFullScreen();
        return true
      }
      return false
    };
    this.appStateListener = (next) => {
      if (next === 'background' || next === 'inactive') {
        this.dismissFullScreen()
      }
    };

    this.touchAction = {
      x: null,
      y: null,
      limitX: null,
      limitY: null,
      slideDirection: null,
      isClick: true
    };

    this.gestureHandlers = {

      onStartShouldSetResponder: (evt) => {
        log('onStartShouldSetResponder');
        return true;
      },

      onMoveShouldSetResponder: (evt) => {
        log('onMoveShouldSetResponder');
        return !this.state.paused;
      },

      onResponderGrant: (evt) => {
        log('onResponderGrant');
      },

      onResponderReject: (evt) => {
        log('onResponderReject');
      },

      onResponderStart: (evt) => {
        log('onResponderStart', evt.nativeEvent.locationX, evt.nativeEvent.locationY);
        this.touchAction.x = evt.nativeEvent.locationX;
        this.touchAction.y = evt.nativeEvent.locationY;
        SystemSetting.getVolume().then((volume) => {
          SystemSetting.getAppBrightness().then((brightness) => {
            this.setState({
              initVolume: volume,
              initBrightness: brightness,
              initTime: this.state.currentTime / this.state.duration,
              tempCurrentTime: this.state.currentTime / this.state.duration,
              showVolumeSlider: false,
              showBrightnessSlider: false,
              showTimeSlider: false
            });
            if (this.sliderTimer) {
              clearTimeout(this.sliderTimer);
              this.sliderTimer = null;
            }
          });
        });
      },

      onResponderMove: (evt) => {
        let formatValue = (v) => {
          if (v > 1.0) return {v: 1.0, outOfRange: true};
          if (v < 0) return {v: 0, outOfRange: true};
          return {v, outOfRange: false}
        };

        let resolveOutOfRange = (outOfRange, props, onSlideBack) => {
          if (outOfRange) {
            if (this.touchAction[props.limit] !== null
              && Math.abs(this.touchAction[props.v] - props.current) < Math.abs(this.touchAction[props.v] - this.touchAction[props.limit])) {
              log('outOfRange', this.touchAction[props.v], props.current, this.touchAction[props.limit]);
              // 用戶把亮度、音量調到極限(0或1.0)後,繼續滑動,並且反向滑動,此時重新定義基準落點
              this.touchAction[props.v] = this.touchAction[props.limit];
              this.touchAction[props.limit] = null;
              if (onSlideBack) onSlideBack()
            } else {
              this.touchAction[props.limit] = [props.current];
            }
          }
        };

        let currentX = evt.nativeEvent.locationX;
        let currentY = evt.nativeEvent.locationY;
        let {fullScreen} = this.state;
        let {videoStyle} = this.props;
        let w = fullScreen ? Dimensions.get('window').width : videoStyle.width;
        let h = fullScreen ? Dimensions.get('window').height : videoStyle.height;

        if (!this.touchAction.slideDirection) {
          if (Math.abs(currentY - this.touchAction.y) > rpx(50)) {
            this.touchAction.slideDirection = 'v'
          } else if (Math.abs(currentX - this.touchAction.x) > rpx(50)) {
            this.touchAction.slideDirection = 'h'
          }
        }

        if (this.touchAction.slideDirection === 'v') {
          log('onResponderMove');
          this.touchAction.isClick = false;
          let dy = this.touchAction.y - currentY;
          let dv = dy / (h / 1.5);
          if (this.touchAction.x > w / 2) {
            let {v, outOfRange} = formatValue(this.state.initVolume + dv);
            log('volume', v, outOfRange);
            this.setState({volume: v, showVolumeSlider: true});
            SystemSetting.setVolume(v);
            resolveOutOfRange(outOfRange, {current: currentY, v: 'y', limit: 'limitY'}, () => {
              this.setState({initVolume: v})
            })
          } else {
            let {v, outOfRange} = formatValue(this.state.initBrightness + dv);
            log('brightness', v, outOfRange);
            this.setState({brightness: v, showBrightnessSlider: true});
            SystemSetting.setAppBrightness(v);
            resolveOutOfRange(outOfRange, {current: currentY, v: 'y', limit: 'limitY'}, () => {
              this.setState({initBrightness: v})
            })
          }
        } else if (this.touchAction.slideDirection === 'h') {
          log('onResponderMove');
          this.touchAction.isClick = false;
          let dx = currentX - this.touchAction.x;
          let dv = dx / (w / 2);
          let {v, outOfRange} = formatValue(this.state.initTime + dv);
          log('time', v, outOfRange);
          this.setState({tempCurrentTime: v, showTimeSlider: true});
          resolveOutOfRange(outOfRange, {current: currentX, v: 'x', limit: 'limitX'}, () => {
            this.setState({initTime: v})
          })
        }
      },

      onResponderRelease: (evt) => {
        log('onResponderRelease');
      },

      onResponderEnd: (evt) => {
        let currentX = evt.nativeEvent.locationX;
        let currentY = evt.nativeEvent.locationY;
        let {fullScreen, tempCurrentTime, duration} = this.state;
        if (Math.abs(currentX - this.touchAction.x) <= rpx(50) && this.touchAction.isClick) {
          // 觸發點擊
          if (this.state.paused) {
            this.play()
          } else {
            this.onTouchedScreen()
          }
        }
        if (!this.touchAction.isClick) {
          this.setState({initVolume: this.state.volume, initBrightness: this.state.brightness});
        }
        this.sliderTimer = setTimeout(() => {
          this.sliderTimer = null;
          this.setState({showVolumeSlider: false, showBrightnessSlider: false, tempCurrentTime: null})
        }, 1000);
        if (tempCurrentTime != null) {
          let time = Math.ceil(tempCurrentTime * duration);
          this.onSeek(time);
          this.player.seek(time);
          this.setState({enableSetTime: true, showTimeSlider: false})
        } else {
          this.setState({showTimeSlider: false})
        }
        this.touchAction = {
          x: null,
          y: null,
          slideDirection: null,
          limitX: null,
          limitY: null,
          isClick: true
        };
      },

      onResponderTerminationRequest: (evt) => {
        log('onResponderTerminationRequest');
        return true;
      },

      onResponderTerminate: (evt) => {
        log('onResponderTerminate');
      }
    }
  }

  componentDidMount() {
    // Orientation.addOrientationListener(this._orientationDidChange);
    BackHandler.addEventListener('hardwareBackPress', this.backListener);
    AppState.addEventListener('change', this.appStateListener);
    if (this.props.cover) {
      this.setState({canShowCover: true})
    }
  }

  _orientationDidChange = (orientation) => {
    if (orientation === 'PORTRAIT') {
      return
    }
    this.isL = orientation === 'LANDSCAPE'
  };

  componentWillUnmount() {
    BackHandler.removeEventListener('hardwareBackPress', this.backListener);
    AppState.removeEventListener('change', this.appStateListener);
    // Orientation.removeOrientationListener(this._orientationDidChange);
  }

  formatVideoStyle(videoStyle) {
    let r = Object.assign({}, videoStyle);
    r.position = 'relative';
    r.zIndex = 99999;
    return r;
  }

  formatFullScreenContainer() {
    const w = Dimensions.get('window').width;
    const h = Dimensions.get('window').height;
    return {
      position: 'absolute',
      left: 0,
      top: 0,
      width: this.isL ? w : h,
      height: this.isL ? h : w,
      zIndex: 99999,
      backgroundColor: '#000'
    }
  }

  onPreNavigate = () => {
    if (!this.state.paused) {
      this.pause()
    }
  };

  onTouchedScreen = () => {
    if (this.state.isTouchedScreen) {
      this.setState({isTouchedScreen: false});
      clearTimeout(this.touchTimer);
      return
    }
    this.setState({isTouchedScreen: !this.state.isTouchedScreen}, () => {
      if (this.state.isTouchedScreen) {
        this.touchTimer = setTimeout(() => {
          this.touchTimer = null;
          this.setState({isTouchedScreen: false})
        }, 10000)
      }
    })
  };

  play() {
    this.setState({
      canShowCover: false,
      paused: !this.state.paused,
    })
  }

  pause() {
    this.setState({
      // canShowCover: false,
      paused: true,
    })
  }

  setDuration(duration) {
    this.setState({duration: duration.duration})
  }

  onLoad(duration) {
    this.setDuration(duration);
  }

  onError(err) {
    log('onError', err)
  }

  onSeek(value) {
    //this.setState({currentTime: value});

    if (this.props.maxPlayTime && this.props.maxPlayTime <= value) {
      this.setState({paused: true, currentTime: 0});
      this.player.seek(0);
      if (this.props.onReachMaxPlayTime) this.props.onReachMaxPlayTime()
    } else {
      this.setState({currentTime: value, enableSetTime: false});
    }
  }

  onEnd(data) {
    //this.player.seek(0);
    this.setState({paused: true, currentTime: 0}, () => {
      this.player.seek(0);
    })
  }

  setTime(data) {
    let sliderValue = parseInt(this.state.currentTime);
    if (this.state.enableSetTime) {
      this.setState({
        slideValue: sliderValue,
        currentTime: data.currentTime,
      });
    }
    if (this.props.maxPlayTime && this.props.maxPlayTime <= data.currentTime) {
      this.setState({paused: true, currentTime: 0});
      this.player.seek(0);
      if (this.props.onReachMaxPlayTime) this.props.onReachMaxPlayTime()
    }
  }

  formatMediaTime(duration) {
    let min = Math.floor(duration / 60);
    let second = duration - min * 60;
    min = min >= 10 ? min : '0' + min;
    second = second >= 10 ? second : '0' + second;
    return min + ':' + second
  }

  dismissFullScreen() {
    // if (Platform.OS === 'android') {
    //   this.player.dismissFullscreenPlayer();
    // }
    if (this.props.onFullScreenChange) {
      this.props.onFullScreenChange(false)
    }
    Orientation.lockToPortrait();
    clearTimeout(this.touchTimer);
    this.setState({fullScreen: false, isTouchedScreen: false})
  }

  toggleFullScreen() {
    if (this.player) {
      let {fullScreen} = this.state;
      if (this.props.onFullScreenChange) {
        this.props.onFullScreenChange(!fullScreen)
      }
      if (fullScreen) {
        // if (Platform.OS === 'android') {
        //   this.player.dismissFullscreenPlayer();
        // }
        this.isL = false;
        Orientation.lockToPortrait()
      } else {
        if (Platform.OS === 'android') {
          this.player.presentFullscreenPlayer();
        }
        this.isL = true;
        Orientation.lockToLandscape()
      }
      clearTimeout(this.touchTimer);
      this.setState({fullScreen: !fullScreen, isTouchedScreen: false})
    }
  }

  render() {
    let {url, videoStyle, title} = this.props;
    let {paused, fullScreen, isTouchedScreen, duration, showVolumeSlider, showBrightnessSlider, showTimeSlider} = this.state;
    let {volume, brightness, tempCurrentTime} = this.state;
    const w = fullScreen ? Dimensions.get('window').width : videoStyle.width;
    let containerStyle = this.formatVideoStyle(videoStyle);
    return (
      <View style={fullScreen ? this.formatFullScreenContainer() : containerStyle}
            activeOpacity={1}>
        <Video source={{uri: url}} //require('../img/English_sing.mp3')
               ref={ref => this.player = ref}
               rate={1}
               volume={1.0}
               muted={false}
               paused={paused}
               resizeMode="contain"
               repeat={false}
               playInBackground={false}
               playWhenInactive={false}
               ignoreSilentSwitch={"ignore"}
               progressUpdateInterval={250.0}
               style={[fullScreen ? this.formatFullScreenContainer() : styles.videoPlayer]}
               onSeek={() => {
               }}
               onLoad={data => this.onLoad(data)}
               onError={(data) => this.onError(data)}
               onProgress={(data) => this.setTime(data)}
               onEnd={(data) => this.onEnd(data)}
        />

        {
          paused ?
            <TouchableOpacity activeOpacity={0.8}
                              style={[fullScreen ? this.formatFullScreenContainer() : styles.videoPlayer, {
                                backgroundColor: 'rgba(0, 0, 0, 0.5)',
                                display: 'flex',
                                alignItems: 'center',
                                justifyContent: 'center'
                              }]}
                              onPress={() => {
                                this.play()
                              }}>
              {this.state.canShowCover ? <Image style={styles.videoPlayer} source={{uri: this.props.cover}}/> : null}
              <Image style={{width: rpx(75), height: rpx(75)}} source={require("./../img/play_video_icon.png")}/>
            </TouchableOpacity> : null
        }

        <View style={[fullScreen ? this.formatFullScreenContainer() : containerStyle, {backgroundColor: 'transparent'}]}
              pointerEvents={paused ? 'box-none' : 'auto'} {...this.gestureHandlers} />

        {
          showVolumeSlider &&
          <View style={[styles.verticalSliderContainer, {position: 'absolute', top: rpx(120), left: 0, right: 0}]}>
            <View style={styles.verticalSlider}>
              <Image style={{width: rpx(32), height: rpx(32)}}
                     source={volume <= 0 ? require('../img/no_volume.png') : require('../img/volume.png')}/>
              <View style={{height: rpx(4), width: rpx(192), flexDirection: 'row'}}>
                <View style={{height: rpx(4), width: rpx(192 * volume), backgroundColor: '#ca3839'}}/>
                <View style={{height: rpx(4), flex: 1, backgroundColor: '#787878'}}/>
              </View>
            </View>
          </View>
        }

        {
          showBrightnessSlider &&
          <View style={[styles.verticalSliderContainer, {position: 'absolute', top: rpx(120), left: 0, right: 0}]}>
            <View style={styles.verticalSlider}>
              <Image style={{width: rpx(32), height: rpx(32)}}
                     source={require('../img/brightness.png')}/>
              <View style={{height: rpx(4), width: rpx(192), flexDirection: 'row'}}>
                <View style={{height: rpx(4), width: rpx(192 * brightness), backgroundColor: '#ca3839'}}/>
                <View style={{height: rpx(4), flex: 1, backgroundColor: '#787878'}}/>
              </View>
            </View>
          </View>
        }

        {
          showTimeSlider &&
          <View style={[styles.verticalSliderContainer, {position: 'absolute', top: rpx(120), left: 0, right: 0}]}>
            <View style={styles.smallSlider}>
              <Text style={{
                color: 'white',
                fontSize: rpx(26)
              }}>{this.formatMediaTime(Math.floor(tempCurrentTime * duration))}/{this.formatMediaTime(Math.floor(duration))}</Text>
            </View>
          </View>
        }
        {
          isTouchedScreen ?
            <View style={[styles.navContentStyle, {width: w, height: fullScreen ? rpx(160) : rpx(90)}]}>
              <LinearGradient colors={['#666666', 'transparent']}
                              style={{flex: 1, height: fullScreen ? rpx(160) : rpx(90), opacity: 0.5}}/>
              <View style={[styles.navContentStyleInner]}>
                {
                  fullScreen ?
                    <TouchableOpacity
                      style={{width: rpx(22), height: rpx(40), marginLeft: rpx(30)}}
                      onPress={() => {
                        this.toggleFullScreen()
                      }}>
                      <Image style={{width: rpx(22), height: rpx(40)}}
                             source={require('../img/back_arrow_icon_white.png')}/>
                    </TouchableOpacity> : null
                }
                {
                  fullScreen ?
                    <TouchableOpacity onPress={() => {
                      this.toggleFullScreen()
                    }}>
                      <Text
                        style={{
                          backgroundColor: 'transparent',
                          color: 'white',
                          marginLeft: rpx(50)
                        }}>{title}</Text>
                    </TouchableOpacity>
                    : null
                }
              </View>

            </View> : <View style={{height: Platform.OS === 'ios' ? 44 : 56, backgroundColor: 'transparent'}}/>
        }


        {
          isTouchedScreen &&
          <View style={[styles.toolBarStyle, {width: w, height: fullScreen ? rpx(110) : rpx(90)}]}>
            <LinearGradient colors={['transparent', '#666666']}
                            style={{flex: 1, height: fullScreen ? rpx(110) : rpx(90), opacity: 0.5}}/>
            <View style={[styles.toolBarStyleInner, {width: w}]}>
              <TouchableOpacity activeOpacity={0.8} onPress={() => this.play()}>
                <Image style={{width: rpx(50), height: rpx(50)}}
                       source={this.state.paused ? require('./../img/play.png') : require('./../img/pause.png')}/>
              </TouchableOpacity>
              <View style={styles.progressStyle}>
                <Text style={styles.timeStyle}>{this.formatMediaTime(Math.floor(this.state.currentTime))}</Text>
                <Slider
                  style={styles.slider}
                  value={this.state.slideValue}
                  maximumValue={this.state.duration}
                  minimumTrackTintColor={'#ca3839'}
                  maximumTrackTintColor={'#989898'}
                  thumbImage={require('../img/slider.png')}
                  step={1}
                  onValueChange={value => this.onSeek(value)}
                  onSlidingComplete={value => {
                    this.player.seek(value);
                    this.setState({enableSetTime: true})
                  }}
                />
                <View style={{flexDirection: 'row', justifyContent: 'flex-end', width: rpx(70)}}>
                  <Text style={{
                    color: 'white',
                    fontSize: rpx(24)
                  }}>{this.formatMediaTime(Math.floor(duration))}</Text>
                </View>
              </View>
              {
                fullScreen ?
                  <TouchableOpacity activeOpacity={0.8} onPress={() => {
                    this.toggleFullScreen()
                  }}>
                    <Image style={{width: rpx(50), height: rpx(50)}} source={require("./../img/not_full_screen.png")}/>
                  </TouchableOpacity> :
                  <TouchableOpacity activeOpacity={0.8} onPress={() => {
                    this.toggleFullScreen()
                  }}>
                    <Image style={{width: rpx(50), height: rpx(50)}} source={require("./../img/full_screen.png")}/>
                  </TouchableOpacity>
              }
            </View>
          </View>
        }
      </View>
    )
  }
}

const styles = StyleSheet.create({

  fullScreenContainer: {
    position: 'absolute',
    left: 0,
    top: 0,
    right: 0,
    bottom: 0
  },
  videoPlayer: {
    position: 'absolute',
    left: 0,
    top: 0,
    right: 0,
    bottom: 0
  },
  toolBarStyle: {
    position: 'absolute',
    left: 0,
    bottom: 0,
    flexDirection: 'row',
    alignItems: 'center',
    height: rpx(110),
    zIndex: 100000
  },
  toolBarStyleInner: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: rpx(30),
    justifyContent: 'space-around',
    flex: 1,
    height: rpx(90),
    position: 'absolute',
    top: 0
  },
  slider: {
    flex: 1,
    marginHorizontal: 5,
    height: rpx(90)
  },
  progressStyle: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-around',
    marginHorizontal: rpx(20)
  },
  timeStyle: {
    width: rpx(70),
    color: 'white',
    fontSize: rpx(24)
  },
  navContentStyle: {
    height: rpx(90),
    flexDirection: 'row',
    alignItems: 'center',
    position: 'absolute',
    top: 0,
    zIndex: 100001,
  },
  navContentStyleInner: {
    height: rpx(90),
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingHorizontal: rpx(20),
    position: 'absolute'
  },
  verticalSlider: {
    width: rpx(320),
    height: rpx(84),
    borderRadius: rpx(42),
    backgroundColor: 'rgba(0,0,0,0.4)',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingHorizontal: rpx(40)
  },
  smallSlider: {
    width: rpx(220),
    height: rpx(84),
    borderRadius: rpx(42),
    backgroundColor: 'rgba(0,0,0,0.4)',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
  },
  verticalSliderContainer: {
    height: rpx(84),
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 100001
  }
});

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章