美文网首页React Native学习ReactNativeFromAndroid
React Native两天开发一个游戏视频播放应用

React Native两天开发一个游戏视频播放应用

作者: Young_Allen | 来源:发表于2018-11-19 09:27 被阅读9次

每天一篇系列:
强化知识体系,查漏补缺。
欢迎指正,共同学习!

1.调试环境

PC : Windows

OS : Android

2.环境准备

Android调试环境准备,有过Android开发经验的开发对Android调试环境不会陌生。JAVA环境、SDK配置、Android Studio环境准备,这些都是基本功,先把这个环境准备好.不是本章节的重点

注意:

1.Android SDK在23以上(app\build.gradle中的compileSdkVersion)

2.Android sdk build tools 在23.0.1以上(app\build.gradle中的buildToolsVersion)

3.Android Support Repository在17以上

4.Android NDK-r10e配置

参考配置:

在gradle构建的项目中,build.gradle配置的版本在Android Studio的SDK Manager中要相应的安装好

3.ReactNative

项目源码位置

https://github.com/facebook/react-native

通常如果安装和配置好git后,可以使用git clone这个项目,或者直接clone or download这个项目,推荐使用git,毕竟是搞开发的,这些工具还是要会用的,也能更好的管理项目。使用git clone好项目后,是没有Node_Modules模块的(react-native的核心库),为了配置Node_Modules需要先配置node.js下载地址:https://nodejs.org/en/版本最好在4.1以上,安装好后可以通过node -v测试是否安装成功

npm安装:

sudo npm install

安装完后,进入react-native根目录:

执行nmp install指令,nmp install会根据同级目录下的package.json下载依赖的node_module,这样就配置好运行需要的基本环境了。

4.Run App

我这里要说的Run App是跑起来自己实现的React-Native APP。

网站上有很多关于在目标终端启动React-Native Example或第一个Hello World的说明,我这里就不作说明了,可以先自己跑起来Hello World应用后,在来看我这里的说明,多少应该有些借鉴意义。

这篇文章是我关于React Native写的第一篇文章,也是我开发第一个React Native应用后写的文章。

首先我的技术栈是Android framework,在实际工作中也会基于原生API开发应用,但是我所开发的应用仅在Android 4.4这样的平台使用而已业务简单,至于如今火热的RxJava,Retrofit、Glide等框架用不上,也没有接触过React Native开发,我自认为是与Android生态脱轨了,现在想一步一步拾起来。

最后介绍下我的项目,这是一个通过分析实际网络交互后开发的一款视频应用,我分析了战旗直播的交互,最终目标是获取游戏视频的播放地址。

先来看一下效果:

image

接下来说下代码:

1.首先创建好React Native工程
新版的RN已经没有了index.android.js和index.ios.js,在index.js中已经统一到APP.js中:

/** @format */
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);

这里我要说一下React Native的编程,使用了JSX语法,可以理解为JS+XML,我开始也很担心编程是个问题,但是我用了两天可以实现这个界面的编程及显示,我觉得熟练了就好。

2.为了方便页面跳转,我选择了react-navigation组件
我创建了一个StackNavigator,用来声明所有页面:

import React, {
  Component
} from 'react';

import {
  StackNavigator
} from 'react-navigation'

import MyCategaryPage from './MyCategaryPage'
import MyPosterPage from './MyPosterPage'
import MyPlayer from './MyPlayer'

const MyNavigator = StackNavigator({
    MyCategaryPage:{
      screen:MyCategaryPage,
      navigationOptions: {
          header:null //隐藏标题栏
      }
    },
    MyPosterPage:{
      screen:MyPosterPage,
      navigationOptions: {
          header:null //隐藏标题栏
      }
    },
    MyPlayer:{
      screen:MyPlayer,
      navigationOptions: {
          header:null //隐藏标题栏
      }
    }
});
export default MyNavigator;

使用react-navigation的好处就是可以方便页面的跳转和返回,比如跳转到MyPosterPage页面,并且可以携带参数跳转:

this.props.navigation.navigate('MyPosterPage',{id:item.id});

页面的返回处理我也统一到一个父类里面:

import React, { Component } from 'react';
import { BackHandler } from 'react-native';
const navigation_stack = [];
class BackInterface extends Component {

    constructor(props) {
        super(props);
        BackHandler.addEventListener('hardwareBackPress', this.go_back.bind(this));     
    }

    addNavigationStack(item){
        //无法正常跳转
        //navigation_stack.push(item);
    }

    go_back() {

        if (this.props.navigation) {
            if(navigation_stack.length > 0){
                var item = navigation_stack.pop();  //通过Name也不能跳转
                this.props.navigation.goBack(item);
            }else{
                //无法正常回退到首页.只能在传null时可以正常返回
                //this.props.navigation.goBack();
                this.props.navigation.goBack(null);
            }
            return true;
        }
        return false;
    }
}
module.exports = BackInterface;

直接使用this.props.navigation.goBack();不能正常从第三级页面返回两次,本来打算通过一个Stack数据结构来实现跳转到指定页,但是也不行,最后只有通过传null参数实现随机跳转。当然子页面需要继承这个类。
3.分析战旗直播的交互

推荐用chrome来看这些交互文件,并且可以模拟手机端的交互。看到有效请求的地址和参数:

这样就知道如何拿到这些游戏分类的数据了。

游戏分类页的源码如下:

//获取全部游戏分类视频  400-1 表示1次获取400个数据
var REQUEST_URL = 'https://m.zhanqi.tv/api/static/game.lists';
var perNum = 400;
var pageNum = 1;

以上在分析战旗竞技交互时得出的数据。如果有直接可以对接的API这些就不需要自己来分析,我这里只是没有已经存在的API可以对接,所以就自己动手扒了一下网页的数据交互过程。
其次就是分类栏的界面显示:

let cols = 3;
let gap = 10;

let ScreenWidth = Dimensions.get('window').width;
let ImageWidth = (ScreenWidth - (cols + 1) * gap) / cols;

class MyCategaryPage extends BackInterface {

  constructor(props) {
    super(props);
    super.addNavigationStack('MyCategaryPage');
    this.state = {
      refreshing: false,
      dataArray: [],
    };
    this.loadData = this.loadData.bind(this);
    this.select_item = this.select_item.bind(this);
  }

  componentDidMount() {
    this.loadData();
  }

  loadData() {
    fetch(REQUEST_URL + '/' + perNum + '-' + pageNum +'.json')
    .then((response) => response.json())
    .then((responseData) => {
      let data = responseData.data.games;
      this.state.dataArray = [];
      this.setState({
        dataArray:this.state.dataArray.concat(data),
      });
    }).catch((error) => {

    }).done();
  }

  render() {
    return (
      <FlatList
      data={this.state.dataArray}
      renderItem={this.renderItem.bind(this)}
      onRefresh={this.onRefresh.bind(this)}
      refreshing={this.state.refreshing}
      horizontal={false}
      numColumns={cols}
      />
      );
  }

  onRefresh(){
   this.loadData();
 }


 renderItem(categary) {
  var item = categary.item;
  return (
    <TouchableOpacity style={styles.container} onPress={() => {this.select_item(item)}}>
    <View style={styles.innerViewStyle}>

    <Image
    source={{uri:item.spic}}
    style={styles.iconStyle}>
    </Image>

    <Text numberOfLines={1} style={styles.textStyle}>{item.name}</Text>

    </View>
    </TouchableOpacity>
    );
}

使用FlatList实现了一个GridView的效果,另外也对下拉刷新做了处理。
当用户点击某一个游戏分类图标时,响应onPress:

select_item(item) {
    //通过navigation传递参数
    this.props.navigation.navigate('MyPosterPage',{id:item.id});
}

这样就通过navigation跳转到MyPosterPage页面:

class MyPosterPage extends BackInterface {

  constructor(props) {
    super(props);
    super.addNavigationStack('MyPosterPage');
    this.state = {
      refreshing: false,
      dataArray: [],
      showFoot: 0,           //0:隐藏footer  1:已加载完成,没有更多数据
      isRefreshing: false,   //下拉控制
    };
    this.loadData = this.loadData.bind(this);
    this.player_video = this.player_video.bind(this);
  }

  componentDidMount() {
    this.loadData();
  }

  loadData() {
    if(pageNo > totalPage){
      return;
    }
    //通过navigation获取参数
    const {params} = this.props.navigation.state;
    const id = params.id;

    fetch(REQUEST_URL + '/' + id + '/' + perNum + '-' + pageNo + '.json')
    .then((response) => response.json())
    .then((responseData) => {
      //下拉刷新
      if(pageNo == 1){
        this.state.dataArray = [];
      } 
      let cnt = responseData.data.cnt;
      let data = responseData.data.rooms;

      let dataBlob = [];
      let i = itemNo;
      let page = parseInt(cnt/perNum);
      totalPage = cnt>perNum?(cnt%perNum?page + 1:page):1;
      //console.warn('cnt:'+cnt+' totalPage:'+totalPage);

      data.map(function (item) {
        dataBlob.push({
          key: i,
          value: item,
        })
        i++;
      });
      //使用Map保证不重复
      itemNo = i;

      let foot = 0;
      if(pageNo >= totalPage){
        foot = 1;
      }

      this.setState({
        dataArray:this.state.dataArray.concat(dataBlob),
        showFoot:foot,
        isRefreshing:false,
      });
      data = null;
      dataBlob = null;
    }).catch((error) => {

    }).done();
  }  
  ...
}

页面跳转后又可以通过一个FlatList来显示游戏视频的列表,而且在这个页面对上拉加载、下拉刷新分别做了处理,navigation传递的参数通过:

    const {params} = this.props.navigation.state;
    const id = params.id;

获取。对每一项游戏视频信息的绘制如下:

renderItem(poster) {
  var item = poster.item.value;
  return (
    <TouchableOpacity style={styles.container} onPress={() => {this.player_video(item)}}>
    <View style={styles.container}>
    <Image
    source={{uri:item.spic}}
    style={styles.small}>
    </Image>
    <View style={styles.rightContainer}>
    <Text style={styles.title}>{item.title}</Text>
    <Text style={styles.introduce}>{"主播:"+item.nickname}</Text>
    <Text style={styles.introduce}>{"观看人数:"+item.online}</Text>
    </View>
    </View>
    </TouchableOpacity>
    );
}

用户点击某个游戏视频信息栏时,可跳转到播放页面:

player_video(item) {
    //通过navigation传递参数
    this.props.navigation.navigate('MyPlayer',{play_url: PLAY_URL + '/' + item.videoId +'.m3u8'});
}

这里的播放地址是我分析战旗竞技交互得来的,item.videoId是在请求游戏视频信息时携带的。

播放器我使用了react-native-video,绑定的是ExoPlayer,也可以绑定Android的MediaPlayer,播放器代码如下:

'use strict';
import React, {
  Component
} from 'react';

import {
  AppRegistry,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
  BackHandler
} from 'react-native';

import Video from 'react-native-video';
import BackInterface from './BackInterface';

class MyPlayer extends BackInterface {
  constructor(props) {
    super(props);
    super.addNavigationStack('MyPlayer');
  }

  state = {
    rate: 1,
    volume: 1,
    muted: false,
    resizeMode: 'contain',
    duration: 0.0,
    currentTime: 0.0,
    paused: true,
  };

  video: Video;

  onLoad = (data) => {
    this.setState({ duration: data.duration });
  };

  onProgress = (data) => {
    this.setState({ currentTime: data.currentTime });
  };

  onEnd = () => {
    this.setState({ paused: true })
    this.video.seek(0)
  };

  onAudioBecomingNoisy = () => {
    this.setState({ paused: true })
  };

  onAudioFocusChanged = (event: { hasAudioFocus: boolean }) => {
    this.setState({ paused: !event.hasAudioFocus })
  };

  getCurrentTimePercentage() {
    if (this.state.currentTime > 0) {
      return parseFloat(this.state.currentTime) / parseFloat(this.state.duration);
    }
    return 0;
  };

  renderRateControl(rate) {
    const isSelected = (this.state.rate === rate);

    return (
      <TouchableOpacity onPress={() => { this.setState({ rate }) }}>
        <Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
          {rate}x
        </Text>
      </TouchableOpacity>
    );
  }

  renderResizeModeControl(resizeMode) {
    const isSelected = (this.state.resizeMode === resizeMode);

    return (
      <TouchableOpacity onPress={() => { this.setState({ resizeMode }) }}>
        <Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
          {resizeMode}
        </Text>
      </TouchableOpacity>
    )
  }

  renderVolumeControl(volume) {
    const isSelected = (this.state.volume === volume);

    return (
      <TouchableOpacity onPress={() => { this.setState({ volume }) }}>
        <Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
          {volume * 100}%
        </Text>
      </TouchableOpacity>
    )
  }

  render() {
    const flexCompleted = this.getCurrentTimePercentage() * 100;
    const flexRemaining = (1 - this.getCurrentTimePercentage()) * 100;

    //通过navigation获取参数
    const {params} = this.props.navigation.state;
    const url = params.play_url;

    return (
      <View style={styles.container}>
        <TouchableOpacity
          style={styles.fullScreen}
          onPress={() => this.setState({ paused: !this.state.paused })}
        >
          <Video
            ref={(ref: Video) => { this.video = ref }}
            /* For ExoPlayer */
            source={{ uri:url}}
            /*source={require('./broadchurch.mp4')}*/
            style={styles.fullScreen}
            rate={this.state.rate}
            paused={this.state.paused}
            volume={this.state.volume}
            muted={this.state.muted}
            resizeMode={this.state.resizeMode}
            onLoad={this.onLoad}
            onProgress={this.onProgress}
            onEnd={this.onEnd}
            onAudioBecomingNoisy={this.onAudioBecomingNoisy}
            onAudioFocusChanged={this.onAudioFocusChanged}
            repeat={false}
          />
        </TouchableOpacity>

        <View style={styles.controls}>
          <View style={styles.generalControls}>
            <View style={styles.rateControl}>
              {this.renderRateControl(0.25)}
              {this.renderRateControl(0.5)}
              {this.renderRateControl(1.0)}
              {this.renderRateControl(1.5)}
              {this.renderRateControl(2.0)}
            </View>

            <View style={styles.volumeControl}>
              {this.renderVolumeControl(0.5)}
              {this.renderVolumeControl(1)}
              {this.renderVolumeControl(1.5)}
            </View>

            <View style={styles.resizeModeControl}>
              {this.renderResizeModeControl('cover')}
              {this.renderResizeModeControl('contain')}
              {this.renderResizeModeControl('stretch')}
            </View>
          </View>

          <View style={styles.trackingControls}>
            <View style={styles.progress}>
              <View style={[styles.innerProgressCompleted, { flex: flexCompleted }]} />
              <View style={[styles.innerProgressRemaining, { flex: flexRemaining }]} />
            </View>
          </View>
        </View>
      </View>
    );
  }
}


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'black',
  },
  fullScreen: {
    position: 'absolute',
    top: 0,
    left: 0,
    bottom: 0,
    right: 0,
  },
  controls: {
    backgroundColor: 'transparent',
    borderRadius: 5,
    position: 'absolute',
    bottom: 20,
    left: 20,
    right: 20,
  },
  progress: {
    flex: 1,
    flexDirection: 'row',
    borderRadius: 3,
    overflow: 'hidden',
  },
  innerProgressCompleted: {
    height: 20,
    backgroundColor: '#cccccc',
  },
  innerProgressRemaining: {
    height: 20,
    backgroundColor: '#2C2C2C',
  },
  generalControls: {
    flex: 1,
    flexDirection: 'row',
    borderRadius: 4,
    overflow: 'hidden',
    paddingBottom: 10,
  },
  rateControl: {
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'center',
  },
  volumeControl: {
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'center',
  },
  resizeModeControl: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
  },
  controlOption: {
    alignSelf: 'center',
    fontSize: 11,
    color: 'white',
    paddingLeft: 2,
    paddingRight: 2,
    lineHeight: 12,
  },
});

export default MyPlayer;

播放器的源码都是些基本代码,播控条UI,全屏等目前没有实现,当然这部分并不是我想要急切实现的内容,因为我之前扩展过原生API接口的ExoPlayer,我比较感兴趣的是react-native-video做了什么,是如何让ExoPlayer和MediaPlayer在React Native中可以这么方便的的使用?

源码Github地址:https://github.com/WoYang/GameVideo_RN

欢迎Fork,有什么好的建议或者指导,欢迎留言。

声明:本人只是对于React Native技术的喜好才使用了战旗直播的文字、图片以及视频资源,本人承诺禁止使用到任何商业用途。

相关文章

网友评论

    本文标题:React Native两天开发一个游戏视频播放应用

    本文链接:https://www.haomeiwen.com/subject/oqzmfqtx.html