基于react native video 实现的一个播放器

·  阅读 1245

前言

基础环境描述

  • RN 版本0.64

效果展示

在这里插入图片描述

在这里插入图片描述

源码video.tsx

//video.tsx
//所使用的字体图标 Icon https://fontawesome.com/v6.0/icons?d=gallery&p=1&q=back&s=solid&m=free
//侦听设备的方向变化,获取当前方向,锁定为首选方向。  https://github.com/wonday/react-native-orientation-locker
import React, { Component } from 'react';
import { View, Text, StyleSheet, Dimensions, TouchableOpacity, StatusBar, ActivityIndicator } from 'react-native';
import Slider from '@react-native-community/slider';
import Video from 'react-native-video';
import * as _ from 'lodash';
import { s_to_hs } from '@src/utils/common';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Orientation from 'react-native-orientation-locker';
import ThreeSlider from './threeSlider';
import { Avatar } from 'react-native-elements';
import { ScrollView } from 'react-native-gesture-handler';
import { pxToDp } from '@src/utils/stylesKits';
import { Image } from 'react-native-elements/dist/image/Image';
// import { useRoute } from '@react-navigation/native';

const w = Dimensions.get('window').width;
const h = Dimensions.get('window').height;
//延迟关闭菜单的时间
const closeMenuDelay = 5000;

export interface AppProps {
  route?: any;
  navigation?: any;
}

class video extends Component<AppProps, any> {
  timerisShowMenu: any;
  timer: any;
  player: any;
  clickTimes: any;
  douClickTimeOut: any;
  constructor(props: AppProps) {
    super(props);
    this.state = {
      isFullScreen: true, //当前是否是竖屏(true),横屏(false)
      rate: 1, // 暂停播放时为0,以正常速度播放时为1。放慢或加速播放时的其他值,
      videoQuality: 6,
      duration: 0, //视频长度,
      enableSetTime: true,
      slideValue: 0.0, //进度条
      step: 0.01, // 滑动时的最小单位 一格大小
      isPaused: true, //控制播放或者暂停
      isHdMenu: false, //是否显示视频倍数、视频质量选择框
      hdMenuType: 1, //倍率、视频质量  菜单类型 1为倍率、2为 视频质量
      isShowMenu: true, //头部和底部菜单的显示、隐藏
      isLoading: true, //加载框
      currentTime: 0, //当前播放的时间节点
      //A-B点
      isShowAB: false,
      start: 0, //开始点
      end: 0, //结束点
      isShowSliderText: false, //是否在屏幕中间显示
      sliderText: 0,
      //播放倍数
      multipleList: [
        {
          value: 2,
          text: '2.0X'
        },
        {
          value: 1.5,
          text: '1.5X'
        },
        {
          value: 1.25,
          text: '1.25X'
        },
        {
          value: 1,
          text: '1.0X'
        },
        {
          value: 0.75,
          text: '0.75X'
        },
        {
          value: 0.5,
          text: '0.5X'
        }
      ],
      //视频质量
      videoQualityList: [
        {
          value: 5,
          text: '1080P 高码率'
        },
        {
          value: 4,
          text: '1080P 高清'
        },
        {
          value: 3,
          text: '720P 高清'
        },
        {
          value: 2,
          text: '480P 清晰'
        },
        {
          value: 1,
          text: '360 流畅'
        },
        {
          value: 6,
          text: '自动'
        }
      ]
    };
  }

  componentDidMount() {
    console.log(this.props.route.params.videoUri);
    //锁定为竖屏
    Orientation.lockToPortrait();
    console.log(Orientation.getInitialOrientation());
    this.setState({ isFullScreen: true });

    if (this.state.isPaused) {
      return;
    }
    //菜单关闭的定时器
    this.timer = setTimeout(() => {
      //隐藏头部菜单栏
      this.setState({ isShowMenu: false });
    }, closeMenuDelay);
  }

  componentWillUnmount() {
    this.timer && clearTimeout(this.timer);
    this.timerisShowMenu && clearTimeout(this.timerisShowMenu);
    this.douClickTimeOut && clearTimeout(this.timerisShowMenu);
  }

  /**
   * 切换全屏的方法
   * @returns
   */
  switchScreen = () => {
    //切换手机屏幕方向
    if (this.state.isFullScreen) {
      console.log('    //屏幕方向锁定为X轴');
      this.setState({ isFullScreen: false });
      //屏幕方向锁定为X轴
      Orientation.lockToLandscapeLeft();
    } else {
      this.setState({ isFullScreen: true });
      console.log('    //屏幕方向锁定为Y轴');
      //屏幕方向锁定为Y轴
      Orientation.lockToPortrait();
    }
  };

  /**
   * 更新进度条
   * @param {*} data
   */
  setTime = (data: any) => {
    //如果开启了AB点功能,那么重复播放
    if (this.state.isShowAB) {
      if (data.currentTime >= this.state.end) {
        this.player.seek(this.state.start);
      }
    }

    //视频卡顿时显示加载框
    if (this.state.currentTime == data.currentTime && this.state.currentTime > 2) {
      //显示loading
      this.setState({ isLoading: true });
      return;
    }
    this.setState({ isLoading: false });
    this.setState({ currentTime: data.currentTime });
    //更新进度条
    this.setState({
      slideValue: data.currentTime
    });
  };

  /**
   * 播放结束
   */
  onEnd = () => {
    console.log('播放结束');
    //如果开启了AB点功能,那么重复播放
    if (this.state.isShowAB) {
      this.player.seek(this.state.start);
      return;
    }

    this.setState({ isPaused: true });
    //修正进度条
    this.setState({ slideValue: this.state.duration });
    //修正节点,显示结尾图像帧
    this.player.seek(this.state.duration - 2);

    //清除关闭菜单的计时器
    clearTimeout(this.timerisShowMenu);
    //隐藏菜单
    this.setState({ isShowMenu: false });
  };

  /**
   * 控制播放或者暂停
   */
  paused = () => {
    this.setState({ isPaused: !this.state.isPaused });
    //如果视频时暂停的状态,就不关闭菜单
    if (!this.state.isPaused) {
      clearTimeout(this.timerisShowMenu);
      return;
    }
    //菜单栏隐藏的定时器
    clearTimeout(this.timerisShowMenu);
    this.timerisShowMenu = setTimeout(() => {
      //隐藏头部菜单栏
      this.setState({ isShowMenu: false });
      clearTimeout(this.timerisShowMenu);
    }, closeMenuDelay);
  };

  /**
   * 重新播放
   * @returns
   */
  repetPlay = () => {
    //初始化进度条
    this.setState({ slideValue: 0 });
    this.setState({ isPaused: !this.state.isPaused });
  };

  /**
   * 返回按钮
   * @returns
   */
  navigationBack = () => {
    //关闭AB点的功能
    this.setState({ isShowAB: false });
    //切换显示方式
    if (this.state.isFullScreen) {
      //返回上一个页面
      this.props.navigation.goBack();
    } else {
      //切换屏幕
      this.switchScreen();
      //关闭播放倍率、播放质量菜单栏
      this.setState({ isHdMenu: false });
    }
  };

  // ---------------------------------View

  /**
   * 底部菜单栏
   * @returns
   */
  bottomMenu = () => {
    if (this.state.isFullScreen) {
      if (this.state.isShowMenu) {
        return this.portraitBottomMenu();
      }
    } else {
      if (this.state.isShowMenu) {
        return this.fullBottomMenu();
      }
    }
  };

  /**
   * 竖屏底部菜单
   * @returns
   */
  portraitBottomMenu = () => {
    return (
      <View
        style={{
          flexDirection: 'row',
          height: 40,
          alignItems: 'center',
          backgroundColor: 'black',
          position: 'absolute',
          width: '100%',
          bottom: 0,
          zIndex: 3,
          opacity: 0.7
        }}
      >
        {/* 播放 */}
        <TouchableOpacity onPress={this.paused} style={{ width: 30, height: 40, alignItems: 'center', justifyContent: 'center' }}>
          {this.state.isPaused ? <Icon name="play" size={20} color="#FFFFFF" /> : <Icon name="pause" size={20} color="#FFFFFF" />}
        </TouchableOpacity>

        <Slider
          style={{ flex: 1, height: 40 }}
          value={this.state.slideValue}
          minimumValue={0}
          maximumValue={this.state.duration}
          minimumTrackTintColor={'#FF6B9F'}
          maximumTrackTintColor={'#FFFFFF'}
          // thumbImage={require('@/res/images/home2.png')}
          step={this.state.step}
          onValueChange={(value) => this.setState({ slideValue: value, sliderText: value })}
          onSlidingComplete={(value) => {
            console.log('用户释放滑块时调用的回调,无论值是否已更改。当前值作为参数传递给回调处理程序。');
            this.player.seek(value);
            //隐藏
            this.setState({ isShowSliderText: false });
          }}
          onSlidingStart={(value) => {
            console.log('当用户拿起滑块时调用的回调。初始值作为参数传递给回调处理程序。');
            //显示
            this.setState({ isShowSliderText: true });
          }}
        />
        <Text style={{ color: '#FFFFFF', fontSize: 13 }}>
          {this.state.slideValue === 0 && this.state.duration === 0
            ? '00:00/00:00'
            : s_to_hs(this.state.slideValue) + '/' + s_to_hs(this.state.duration)}
        </Text>

        {/* 全屏按钮 */}
        <TouchableOpacity onPress={this.switchScreen} style={{ height: 40, width: 40, alignItems: 'center', justifyContent: 'center' }}>
          <Icon name="expand" size={20} color="#FFFFFF" />
        </TouchableOpacity>
      </View>
    );
  };

  /**
   * 初始化并显示A B点的值
   */
  abInit = () => {
    //暂停播放
    this.setState({ isPaused: true });
    //初始化 a b 点的值
    this.setState({ start: 0, end: this.state.duration });

    //显示 or 隐藏
    this.setState({ isShowAB: !this.state.isShowAB });
  };

  /**
   * 开始移动时
   */
  onGrant() {
    this.setState({ isShowSliderText: true });
  }

  /**
   *   拖动结束的回调
   * @param {*} ishow
   */
  onRelease() {
    //隐藏
    this.setState({ isShowSliderText: false });
  }

  /**
   * A  移动时的回调
   */
  onStartMove(start: any) {
    console.log(start);
    this.setState({ start, sliderText: start });
  }

  /**
   * A点正在移动时Slider不在区域内
   */
  onStartSliderMove(start: any, slideValue: any) {
    this.setState({ start, slideValue, sliderText: start });
    this.player.seek(slideValue);
  }

  /**
   * B 移动时的回调
   */
  onEndMove(end: any) {
    this.setState({ end, sliderText: end });
  }

  /**
   * B点正在移动时Slider不在区域内
   */
  onEndSliderMove(end: any, slideValue: any) {
    this.setState({ end, slideValue, sliderText: end });

    this.player.seek(slideValue);
  }

  /**
   * 播放节点 移动时的回调
   */
  onSlideMove(slideValue: any) {
    this.setState({ slideValue, sliderText: slideValue });
    this.player.seek(slideValue);
  }

  /**
   * threeSlider View
   * @returns
   */
  threeSliderView = () => {
    return (
      <View style={{ flex: 1, height: 70 }}>
        <ThreeSlider
          range={this.state.duration}
          startA={this.state.start}
          endB={this.state.end}
          slideValue={this.state.slideValue}
          onGrant={this.onGrant.bind(this)} //开始移动的回调
          onRelease={this.onRelease.bind(this)} // 移动结束时的回调
          onStartMove={this.onStartMove.bind(this)} //A点正在移动时的回调
          onStartSliderMove={this.onStartSliderMove.bind(this)} //A点正在移动时Slider不在区域内
          onEndMove={this.onEndMove.bind(this)} //B点正在移动时的回调
          onEndSliderMove={this.onEndSliderMove.bind(this)} //B点正在移动时Slider不在区域内的回调
          onSlideMove={this.onSlideMove.bind(this)} //播放正在移动时的回调
        />
      </View>
    );
  };

  /**
   * 全屏情况下默认的进度条
   */
  sildeFullNormalView = () => {
    return (
      <Slider
        style={{ flex: 1, height: 40 }}
        value={this.state.slideValue}
        minimumValue={0}
        maximumValue={this.state.duration}
        minimumTrackTintColor={'#FF6B9F'}
        maximumTrackTintColor={'#FFFFFF'}
        // thumbImage={require('@/res/images/home2.png')}
        step={this.state.step}
        onValueChange={(value) => this.setState({ slideValue: value, sliderText: value })}
        onSlidingComplete={(value) => {
          this.player.seek(value);
          //隐藏
          this.setState({ isShowSliderText: false });
        }}
        onSlidingStart={(value) => {
          console.log('当用户拿起滑块时调用的回调。初始值作为参数传递给回调处理程序。');
          //显示
          this.setState({ isShowSliderText: true });
        }}
      />
    );
  };

  /**
   * 全屏底部菜单
   * @returns
   */
  fullBottomMenu = () => {
    return (
      <View
        style={{
          backgroundColor: 'black',
          position: 'absolute',
          bottom: 0,
          zIndex: 3,
          width: '100%',
          opacity: 0.7
        }}
      >
        <View style={{ flexDirection: 'row', alignItems: 'center' }}>
          <View style={{ flex: 1 }}>
            {/* A-B点  和 默认进度条 */}
            {this.state.isShowAB ? this.threeSliderView() : this.sildeFullNormalView()}
          </View>
        </View>
        <View style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: 'black' }}>
          <TouchableOpacity onPress={this.paused} style={{ width: 30, height: 40, alignItems: 'center', justifyContent: 'center' }}>
            {this.state.isPaused ? <Icon name="play" size={20} color="#FFFFFF" /> : <Icon name="pause" size={20} color="#FFFFFF" />}
          </TouchableOpacity>
          <TouchableOpacity style={{ width: 40, height: 40, alignItems: 'center', justifyContent: 'center' }}>
            <Text style={{ color: '#FFFFFF' }}></Text>
          </TouchableOpacity>
          <TouchableOpacity
            style={{
              flex: 1,
              borderRadius: 5,
              backgroundColor: '#8c8c8c',
              padding: 5,
              opacity: 0.5,
              marginRight: 8
            }}
          >
            <Text style={{ color: '#FFFFFF' }}>发个友善的弹幕见证当下</Text>
          </TouchableOpacity>
          <TouchableOpacity onPress={this.abInit}>
            <Text style={{ color: '#FFFFFF', minWidth: 38 }}>A-B</Text>
          </TouchableOpacity>
          <TouchableOpacity onPress={this.mutiple}>
            <Text style={{ color: '#FFFFFF', minWidth: 38 }}>倍速</Text>
          </TouchableOpacity>
          <TouchableOpacity onPress={this.videoQuality}>
            <Text style={{ color: '#FFFFFF', minWidth: 38 }}>自动</Text>
          </TouchableOpacity>
        </View>
      </View>
    );
  };

  /**
   * 播放倍率
   */
  mutiple = () => {
    this.setState({ isHdMenu: !this.state.isHdMenu, hdMenuType: 1 });
    //隐藏菜单栏
    this.setState({ isShowMenu: false });
  };

  /**
   * 视频质量
   */
  videoQuality = () => {
    this.setState({ isHdMenu: !this.state.isHdMenu, hdMenuType: 2 });
    //隐藏菜单栏
    this.setState({ isShowMenu: false });
  };

  /**
   * 横屏下弹出框:倍速/视频质量
   * @returns
   */
  hdMenu = () => {
    return (
      <View
        style={{
          backgroundColor: 'black',
          zIndex: 3,
          position: 'absolute',
          right: 0,
          height: '100%',
          width: 150,
          opacity: 0.6,
          justifyContent: 'space-around'
        }}
      >
        {this.hdMap()}
      </View>
    );
  };

  /**
   * 菜单渲染
   * 倍数或播放速率的渲染
   * @returns
   */
  hdMap = () => {
    let data = [];
    if (this.state.hdMenuType === 1) {
      data = this.state.multipleList;
    } else {
      data = this.state.videoQualityList;
    }
    return data.map((item: any, index: number) => {
      return (
        <TouchableOpacity
          onPress={() => {
            this.setVideo(item.value);
          }}
          key={index}
          style={{ height: 40, alignItems: 'center', flexDirection: 'row', marginLeft: 20 }}
        >
          <Text
            style={{
              color: item.value == this.state.rate || item.value == this.state.videoQuality ? '#EC7DA0' : '#FFFFFF'
            }}
          >
            {item.text}
          </Text>
          {/* 视频vip图标显示 */}
          {item.value == 'vip' ? this.showVIPIcon() : null}
        </TouchableOpacity>
      );
    });
  };

  /**
   * 渲染视频质量时,显示VIP图标
   */
  showVIPIcon = () => {
    if (this.state.hdMenuType === 2) {
      return <Text style={{ color: 'red', marginLeft: 10 }}>VIP</Text>;
    }
  };

  /**
   * 设置视频播放速度/视频质量
   * @returns
   */
  setVideo = (value: any) => {
    if (this.state.hdMenuType === 1) {
      //设置播放速度
      this.setState({ rate: value });
    } else {
      //切换视频质量
      console.log('切换视频质量为:' + value);
    }

    //关闭菜单
    this.setState({ isHdMenu: !this.state.isHdMenu });
  };

  /**
   * 头部菜单
   * @returns
   */
  headView = () => {
    return (
      <View
        style={{
          backgroundColor: 'black',
          opacity: 0.7,
          position: 'absolute',
          zIndex: 3,
          flexDirection: 'row',
          alignItems: 'center',
          width: '100%'
        }}
      >
        {/* 返回小窗口 */}
        <TouchableOpacity onPress={this.navigationBack} style={{ height: 40, width: 30, alignItems: 'center', justifyContent: 'center' }}>
          <Icon name="angle-left" size={30} color="#FFFFFF" />
        </TouchableOpacity>
        <Text style={{ color: '#FFFFFF', marginLeft: 12 }}> 标题</Text>
      </View>
    );
  };

  /**
   * 中间触控区域
   * @returns
   */
  middleView = () => {
    return (
      <TouchableOpacity
        onPress={() => {
          console.log('middleView');
          this.clickTimes = this.clickTimes || 0;
          this.douClickTimeOut && clearTimeout(this.douClickTimeOut);
          ++this.clickTimes;
          this.douClickTimeOut = setTimeout(() => {
            // 1-单击事件 2-双击事件
            //如果是单击
            if (this.clickTimes === 1) {
              this.menuFun();
            }
            //如果是双击
            if (this.clickTimes === 2) {
              console.log('双击');
              //暂停或开始播放视频
              this.setState({ isPaused: !this.state.isPaused });
              if (this.state.isPaused) {
                this.setState({ isShowMenu: true });
                clearTimeout(this.timerisShowMenu);
                return;
              }
              //菜单栏隐藏的定时器
              clearTimeout(this.timerisShowMenu);
              this.timerisShowMenu = setTimeout(() => {
                //隐藏头部菜单栏
                this.setState({ isShowMenu: false });
                clearTimeout(this.timerisShowMenu);
              }, closeMenuDelay);
            }
            this.clickTimes = 0;
          }, 250);
        }}
        style={{
          position: 'absolute',
          zIndex: 2,
          width: '100%',
          height: '100%'
        }}
      ></TouchableOpacity>
    );
  };

  /**
   * 头部\底部\倍速\视频质量----菜单栏控制
   * @returns
   */
  menuFun = () => {
    // 如果视频处于暂停状态
    // if (!this.state.isPaused) {
    //   this.setState({ isShowMenu: true });
    //   clearTimeout(this.timerisShowMenu);
    //   return;
    // }
    //倍速和视频质量
    if (this.state.isHdMenu) {
      this.setState({ isHdMenu: false });
      return;
    }

    clearTimeout(this.timerisShowMenu);
    //显示菜单
    this.setState({ isShowMenu: true });
    if (this.state.isShowMenu) {
      //菜单栏隐藏的定时器
      this.timerisShowMenu = setTimeout(() => {
        //隐藏头部菜单栏
        this.setState({ isShowMenu: false });
        clearTimeout(this.timerisShowMenu);
      }, closeMenuDelay);
    }
  };

  /**
   * 自制loading
   */
  loadingView = () => {
    return (
      <View
        style={{
          backgroundColor: '#888888',
          // flex: this.state.isFullScreen ? null : 1,
          position: 'absolute',
          zIndex: 4,
          width: '100%',
          opacity: 0.5
        }}
      >
        <ActivityIndicator size="large" color="#FA7198" style={{ height: h * 0.3 }} />
      </View>
    );
  };

  /**
   * 拖动进度条显示播放精度
   */
  showSliderText = () => {
    return (
      <Text
        style={{
          color: '#FFFFFF',
          fontSize: 13,
          position: 'absolute',
          backgroundColor: 'black',
          padding: 5,
          borderRadius: 7,
          zIndex: 10,
          left: '45%',
          top: '45%'
        }}
      >
        {this.state.sliderText === 0 && this.state.duration === 0
          ? '00:00/00:00'
          : s_to_hs(this.state.sliderText) + '/' + s_to_hs(this.state.duration)}
      </Text>
    );
  };

  render() {
    return (
      <View style={{ flex: 1 }}>
        {this.videoView()}
        {/* 竖屏情况下下半部 */}
        {this.state.isFullScreen ? this.bottomView() : null}
      </View>
    );
  }

  /**
   * 底部View
   * 
   * @returns
   */
  bottomView = () => {
    return (
      <View style={{ flex: 7 }}>
        <View style={{ padding: 15 }}>
          <Text style={{ fontSize: 16, lineHeight: 22, color: '#333333', fontWeight: 'bold' }}>跟上步伐,今日打卡学习舞蹈</Text>
          <Text style={{ fontSize: 14, color: '#333333', lineHeight: 23, fontWeight: '400' }}>
            每天都准时打卡练习芭蕾,小仙女儿们,一起来啊!每天一练,做个美美的小仙女儿!
          </Text>
        </View>
        {/* 分割线 */}
        <View style={{ backgroundColor: '#F5F5F5', width: '100%', height: 10 }} />
        <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 15 }}>
          <View style={{ flexDirection: 'row', alignItems: 'center' }}>
            <Avatar
              rounded
              size={60}
              source={{
                uri:
                  'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201710%2F24%2F20171024071632_hzJ8n.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=0d8c0d856224efafbd6a56e9c212be8d'
              }}
            />
            <View style={{ justifyContent: 'space-around', marginLeft: 3 }}>
              <Text style={{ color: '#222222', lineHeight: 22, fontWeight: 'bold', fontSize: 16 }}>天赋舞蹈室</Text>
              <Text style={{ color: '#666666', lineHeight: 17, fontWeight: '400', fontSize: 12, marginTop: 3 }}>用心做舞蹈,开心做自己</Text>
            </View>
          </View>
          <TouchableOpacity
            onPress={() => {
              console.log('回关');
            }}
            style={{
              borderStyle: 'solid',
              borderColor: '#FF5E15',
              borderWidth: 1,
              borderRadius: 15,
              width: 63,
              height: 26,
              justifyContent: 'center',
              alignItems: 'center',
              flexDirection: 'row'
            }}
          >
            <Icon name="plus" size={10} color="#FF5E15" />
            <Text style={{ fontSize: 14, color: '#FF5E15', fontWeight: '400', lineHeight: 20, marginLeft: 2 }}>关注</Text>
          </TouchableOpacity>
        </View>
        {/* 分割线 */}
        <View style={{ backgroundColor: '#F5F5F5', width: '100%', height: 10 }} />
        <ScrollView style={{ padding: 15 }}>
          <View style={{ flexDirection: 'row', alignItems: 'center' }}>
            <Text style={{ lineHeight: 25, fontWeight: 'bold', color: '#222222', fontSize: 18 }}>留言</Text>
            <Text style={{ fontSize: 14, lineHeight: 20, color: '#666666', fontWeight: '400', marginLeft: 5 }}>(共2条)</Text>
          </View>
          {this.commentView()}
        </ScrollView>

        <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 15 }}>
          <TouchableOpacity
            style={{
              backgroundColor: '#F0F0F0',
              borderRadius: 15,
              width: pxToDp(208),
              height: pxToDp(30),
              flexDirection: 'row',
              alignItems: 'center'
            }}
          >
            <Image style={{ width: pxToDp(10), height: pxToDp(16), marginLeft: 10 }} source={require('@/res/images/write.png')} />
            <Text style={{ color: '#999999', lineHeight: 17, fontWeight: '400', fontSize: 12, marginLeft: 2 }}>请输入留言</Text>
          </TouchableOpacity>
          <View style={{ flexDirection: 'row' }}>
            <Icon name="thumbs-up" style={{ marginRight: 10 }} size={16} color="#FF5E15" />
            <Icon style={{ marginRight: 5 }} name="share" size={16} color="#FF5E15" />
            <Text>分享</Text>
          </View>
        </View>
      </View>
    );
  };

  /**
   * 评论
   */
  commentView = () => {
    const data = [
      {
        id: 1,
        headUri:
          'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',

        name: '冥想',
        time: '2021-03-11 13:21:31',
        value: '我也想一起打卡'
      },
      {
        id: 1,
        headUri:
          'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',

        name: '冥想',
        time: '2021-03-11 13:21:31',
        value: '我也想一起打卡'
      },
      {
        id: 1,
        headUri:
          'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',

        name: '冥想',
        time: '2021-03-11 13:21:31',
        value: '我也想一起打卡'
      },
      {
        id: 1,
        headUri:
          'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',

        name: '冥想',
        time: '2021-03-11 13:21:31',
        value: '我也想一起打卡'
      },
      {
        id: 1,
        headUri:
          'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',

        name: '冥想',
        time: '2021-03-11 13:21:31',
        value: '我也想一起打卡'
      },
      {
        id: 1,
        headUri:
          'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',

        name: '冥想',
        time: '2021-03-11 13:21:31',
        value: '我也想一起打卡'
      },
      {
        id: 1,
        headUri:
          'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',

        name: '冥想',
        time: '2021-03-11 13:21:31',
        value: '我也想一起打卡'
      },
      {
        id: 1,
        headUri:
          'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',

        name: '冥想',
        time: '2021-03-11 13:21:31',
        value: '我也想一起打卡'
      }
    ];

    return data.map((item, index) => {
      return this.commentItemView(item, index);
    });
  };

  /**
   * 评论明细
   * @returns
   */
  private commentItemView(item: any, index: number) {
    return (
      <View style={{ marginTop: 10 }} key={index}>
        <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
          <View style={{ flexDirection: 'row', alignItems: 'center' }}>
            <Avatar
              rounded
              source={{
                uri: item.headUri
              }}
            />
            <Text style={{ lineHeight: 21, color: '#666666', fontWeight: '400', fontSize: 15 }}>{item.name}</Text>
          </View>
          <Text style={{ lineHeight: 18, color: '#666666', fontWeight: '400', fontSize: 13 }}>{item.time}</Text>
        </View>
        <Text style={{ lineHeight: 21, color: '#333333', fontWeight: '400', fontSize: 15, marginTop: 5 }}>{item.value}</Text>
      </View>
    );
  }

  /**
   * 播放器
   * @returns
   */
  private videoView() {
    return (
      <View style={{ flex: this.state.isFullScreen ? 3 : 1 }}>
        {/* Loading */}
        {this.state.isLoading ? this.loadingView() : null}
        {/* 播放区域触控 */}
        {this.middleView()}
        {/* 头部菜单 */}
        {this.state.isShowMenu ? this.headView() : this.middleView()}
        {/* 拖动进度条显示播放精度 */}
        {this.state.isShowSliderText ? this.showSliderText() : null}

        {/* 播放器本体 */}
        <Video
          // https://vjs.zencdn.net/v/oceans.mp4
          // http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8
          // http://dg1sy-vodcdn.migucloud.com/mgc_transfiles/4947/2021/3/27/0f8Cmn55zakUGv45Btz0/83300486/custom_origin_4M/0f8Cmn55zakUGv45Btz0custom_origin_4Mhls.mp4.m3u8
          // ../../res/oceans.mp4
          // source={route.params.path}
          // source={require('../../../res/oceans.mp4')}
          source={{
            uri: this.props.route.params.videoUri //在模拟器时无法播放m3u8的视频
          }}
          ref={(ref) => {
            this.player = ref;
          }}
          // bufferConfig={{
          //   minBufferMs: 15000, //播放器将始终确保默认的最小媒体持续时间(以毫秒为单位)
          //   maxBufferMs: 50000, //播放器将尝试缓冲的默认最大媒体持续时间(以毫秒为单位)
          //   bufferForPlaybackMs: 2500, //在用户操作(例如,搜索)之后必须缓冲以供回放以开始或恢复的媒体的默认持续时间(以毫秒为单位)。
          //   bufferForPlaybackAfterRebufferMs: 5000 //必须重新缓冲后才能重新播放的必须缓冲的默认媒体持续时间(以毫秒为单位)。重新缓冲区被定义为缓冲区耗尽而不是用户操作引起的。
          // }}
          paused={this.state.isPaused} //控制播放或者暂停
          style={this.state.isFullScreen ? styles.videoMin : styles.videoMax}
          poster="https://baconmockup.com/300/200/" //视频加载时显示的图片
          posterResizeMode="cover" //图片显示方式
          // controls={true} //显示原生播放控件
          rate={this.state.rate} //播放速度
          resizeMode="cover" //确定当帧与原始视频尺寸不匹配时如何调整视频大小。
          onLoad={(data) => {
            //获取视频长度毫秒
            this.setState({ duration: data.duration });
            this.setState({ isLoading: false });
          }} //加载媒体并准备播放时调用的回调函数。
          progressUpdateInterval={1000} // 两个onProgress事件之间的延迟时间(以毫秒为单位)
          onProgress={(data) => this.setTime(data)} //更新进度条
          onEnd={this.onEnd} //视频播放结束
          onAudioBecomingNoisy={() => {
            this.setState({ isPaused: false });
          }} //由于音频输出变化而使音频即将变得“嘈杂”时调用的回调函数。通常,当音频输出从外部源(如耳机)切换回内部扬声器时,将调用此方法。最好在发生这种情况时暂停媒体播放,以免扬声器开始发出声音。
        />

        {/* 底部菜单 */}
        {this.bottomMenu()}
        {/* 全屏下  底部菜单栏 倍速、视频质量 */}
        {this.state.isHdMenu ? this.hdMenu() : null}
      </View>
    );
  }
}

var styles = StyleSheet.create({
  videoMin: {
    //小窗口
    flex: 1,
    backgroundColor: 'black'
  },
  videoMax: {
    //大窗口
    flex: 1,
    backgroundColor: 'black'
  }
});

export default video;


复制代码

源码threeSlider.tsx


/**
 * 三个点的滑动进度条
 */

// Match参考 https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil
import React, { Component } from 'react';
import { StyleSheet, View, PanResponder, Text, Dimensions } from 'react-native';

const roundSize = 30; // 圆的大小
const w222 = Dimensions.get('window').width - roundSize * 2; // 设备宽度
const h222 = Dimensions.get('window').height - roundSize * 2; // 设备宽度
const width = w222 > h222 ? w222 : h222;

export interface AppProps {
  range: number; //视频长度
  startA: number; //A点位置
  endB: number; //B点位置
  slideValue: number; //当前播放位置
  onGrant: any; //开始移动的回调
  onStartSliderMove: any; //A点开始移动时的回调
  onStartMove: any; //A点正在移动时的回调
  onRelease: any; // 移动结束时的回调
  onEndSliderMove: any; //B点正在移动时Slider不在区域内的回调
  onEndMove: any; //B点正在移动时的回调
  onSlideMove: any; //播放正在移动时的回调
}

class Index extends Component<AppProps, any> {
  panResponderStart: any;
  panResponderEnd: any;
  panResponderPlay: any;
  static defaultProps: {
    // Match参考 https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil
    range: number; // 默认
    startA: number; // 起始
    endB: number;
  };
  constructor(props: AppProps) {
    super(props);
    let scale = width / this.props.range;
    let { range, startA, endB, slideValue } = this.props;
    let start = Math.round(startA === 0 ? roundSize / 2 : startA === range ? width - roundSize : scale * startA);
    let end = Math.round(endB === 0 ? width : scale * endB);
    let slide = Math.round(slideValue === 0 ? roundSize / 2 : slideValue === range ? width : scale * slideValue);
    this.state = {
      range,
      startA,
      endB,
      slideValue,
      start, // 起始坐标
      end, // 结束坐标
      slide
    };
  }

  //父元素对组件的props或state进行了修改
  UNSAFE_componentWillReceiveProps(nextProps: any) {
    let scale = width / this.props.range;
    let { range, startA, endB, slideValue } = nextProps;
    let start = Math.round(startA === 0 ? roundSize / 2 : startA === range ? width - roundSize : scale * startA);
    let slide = Math.round(slideValue === 0 ? roundSize / 2 : slideValue === range ? width : scale * slideValue);
    let end = Math.round(endB === 0 ? width : scale * endB);
    this.setState({
      range,
      startA,
      endB,
      slideValue,
      start, // 起始坐标
      end, // 结束坐标
      slide
    });
  }

  //组件将要被加载到视图之前调用
  UNSAFE_componentWillMount() {
    let scale = width / this.props.range;
    this.panResponderStart = PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      // 开始
      onPanResponderGrant: (evt, gestureState) => {
        this.forceUpdate();
        //显示
        this.props.onGrant();
      },
      onPanResponderMove: (evt, gestureState) => {
        // 开始的拖动事件
        let start = gestureState.moveX; // 当前拖动所在的坐标
        let threshold = this.state.end - roundSize; // 阀值
        if (start >= threshold) {
          // 保证开始价格不会超过结束价格
          start = threshold;
        }

        let startA = Math.round(start / scale); // 计算 实际的值
        // 保证开始价格不会小于最小值
        if (start <= roundSize) {
          start = roundSize / 2;
          startA = 0;
        }

        //修正播放的进度条
        if (start > this.state.slide) {
          //A点正在移动时Slider不在区域内
          let slideValue = Math.ceil(start / scale);
          this.setState({ slide: start, slideValue, start, startA }, () => {
            this.props.onStartSliderMove(this.state.startA, slideValue);
          });
        } else {
          this.setState(
            {
              start,
              startA
            },
            () => {
              this.props.onStartMove(this.state.startA);
            }
          );
        }
      },
      onPanResponderRelease: (evt, gestureState) => {
        // 隐藏
        this.props.onRelease();
        return true;
      },
      onPanResponderTerminate: (evt, gestureState) => true
    });
    this.panResponderEnd = PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {
        this.forceUpdate();
        //显示
        this.props.onGrant();
      },
      onPanResponderMove: (evt, gestureState) => {
        // 结束的拖动事件
        let end = gestureState.moveX;
        let threshold = this.state.start + roundSize; // 阀值
        if (end <= threshold) {
          // 保证开始价格不会超过结束价格
          end = threshold;
        }
        // end = parseInt(end / step) * step;

        let endB = Math.round(end / scale);
        if (end >= width) {
          // 保证结束价格不会超过最大值
          end = width;
          endB = this.state.range;
        }

        //修正播放的进度条
        if (end < this.state.slide) {
          let slideValue = Math.floor(end / scale);
          this.setState({ slide: end, slideValue });
        }

        if (end < this.state.slide) {
          //B点正在移动时Slider不在区域内
          let slideValue = Math.ceil(end / scale);
          this.setState({ slide: end, slideValue, end, endB }, () => {
            this.props.onEndSliderMove(this.state.endB, slideValue);
          });
        } else {
          this.setState(
            {
              end,
              endB
            },
            () => {
              this.props.onEndMove(this.state.endB);
            }
          );
        }
      },
      onPanResponderRelease: (evt, gestureState) => {
        // 隐藏
        this.props.onRelease();
        return true;
      },
      onPanResponderTerminate: (evt, gestureState) => true
    });

    this.panResponderPlay = PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {
        this.forceUpdate();
        //显示
        this.props.onGrant();
      },
      onPanResponderMove: (evt, gestureState) => {
        // 结束的拖动事件
        let slide = gestureState.moveX;
        let slideValue = Math.round(slide / scale);
        //最小值
        if (slide <= this.state.start) {
          // 保证开始价格不会小于最小值
          slide = this.state.start;
          slideValue = this.state.startA;
        }

        //最大值
        if (slide >= this.state.end) {
          // 保证开始价格不会大于最大值
          slide = this.state.end;
          slideValue = this.state.endB;
        }

        this.setState(
          {
            slide,
            slideValue
          },
          () => {
            this.props.onSlideMove(this.state.slideValue);
          }
        );
      },
      onPanResponderRelease: (evt, gestureState) => {
        // 隐藏
        this.props.onRelease();
        return true;
      },
      onPanResponderTerminate: (evt, gestureState) => true
    });
  }

  render() {
    let { start, end, slide } = this.state;
    return (
      <View style={styles.container}>
        <View style={{ flexDirection: 'row' }}>
          <View style={[styles.progressContainer, { backgroundColor: '#D6D7E6' }, { width: start == roundSize / 2 ? 0 : start }]} />
          <View style={[styles.progressContainer, { width: width - start - (width - end) }]} />
          <View style={[styles.progressContainer, { backgroundColor: '#D6D7E6' }, { width: width - end }]} />
        </View>

        <View style={[styles.startA, { left: start }]} {...this.panResponderStart.panHandlers}>
          <Text>A</Text>
        </View>
        <View style={[styles.endB, { left: end }]} {...this.panResponderEnd.panHandlers}>
          <Text>B</Text>
        </View>
        <View style={[styles.slide, { left: slide }]} {...this.panResponderPlay.panHandlers}></View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    height: 70,
    justifyContent: 'center',
    alignItems: 'center'
  },
  progressContainer: {
    backgroundColor: '#ffa710',
    height: 4
  },
  startA: {
    position: 'absolute',
    width: roundSize,
    height: roundSize,
    borderRadius: roundSize / 2,
    borderColor: '#D6D7E6',
    borderWidth: 1,
    shadowColor: 'rgba(0,0,0,0.6)',
    shadowRadius: 5,
    shadowOpacity: 0.9,
    backgroundColor: 'green',
    top: 1,
    justifyContent: 'center',
    alignItems: 'center'
  },
  endB: {
    position: 'absolute',
    width: roundSize,
    height: roundSize,
    borderRadius: roundSize / 2,
    borderColor: '#D6D7E6',
    borderWidth: 1,
    shadowColor: 'rgba(0,0,0,0.6)',
    shadowRadius: 5,
    shadowOpacity: 0.9,
    backgroundColor: 'yellow',
    top: 1,
    justifyContent: 'center',
    alignItems: 'center'
  },
  slide: {
    position: 'absolute',
    width: roundSize,
    height: roundSize,
    borderRadius: roundSize / 2,
    borderColor: '#D6D7E6',
    borderWidth: 1,
    shadowColor: 'rgba(0,0,0,0.6)',
    shadowRadius: 5,
    shadowOpacity: 0.9,
    backgroundColor: '#094D45',
    bottom: 1
  }
});

//默认props
Index.defaultProps = {
  range: 1000, // 默认
  startA: 0, // 起始
  endB: width
};

export default Index;

复制代码

后期计划

  • 如果有时间的话,希望可以把这个播放器做的跟哔哩哔哩一样。

已知bug

  • 在小米手机全面屏情况下,全屏播放时,底部会有白边,暂时不知道怎么解决。

github仓库地址

参考

分类:
Android
收藏成功!
已添加到「」, 点击更改