前两天刚搭建了SRS服务器,正好利用SRS服务器搭建一个音视频通话的APP小demo玩玩,经过了解Android端推流&拉流后成功做出一个比较low的demo,不嫌弃的话可以看一看

在编码和推流,有两个方案选择:

一: 使用javacv来实现,最终也是用过ffmpeg来进行编码和推流,javacv实现到可以直接接收摄像头的帧数据
需要自己实现的代码只是打开摄像头,写一个SurfaceView进行预览,然后实现PreviewCallback将摄像头每一帧的数据交给javacv即可
javacv地址:https://github.com/bytedeco/javacv
demo地址:https://github.com/beautifulSoup/RtmpRecoder/tree/master
二:使用Android自带的编码工具,可实现硬编码,这里有一个国内大神开源的封装很完善的的库yasea,第一种方法需要实现的Camera采集部分也一起封装好了,进行一些简单配置就可以实现编码推流,并且yasea目前已经直接支持摄像头的热切换,和各种滤镜效果
yasea地址(内置demo):https://github.com/begeekmyfriend/yasea

服务器:

流媒体服务器我用的是srs,地址:https://github.com/ossrs/srs/wiki/v3_CN_Home
关于srs的编译、配置、部署、在官方wiki中已经写的很详细了,并且srs同样是国内开发人员开源的项目,有全中文的文档,看起来很方便
这里有最基本的简单编译部署过程 CentOS 7 JavaWeb 环境下SRS+Nginx搭建流媒体服务器

播放器:

android端的播放使用哔哩哔哩开源的ijkplayer播放器,vitamio播放器也可以
ijkplayer地址:https://github.com/Doikki/DKVideoPlayer
vitamio地址(内置demo):https://github.com/yixia/VitamioBundle

准备开源库:

ijkplayer + yasea
这里的 yasea 使用Module方式导入,原因是我没找到别的导入方式

直播实现的流程:

  1. 使用yaesa进行摄像头采集、编码然后向srs服务器rtmp推流
  2. 部署srs流媒体服务器
  3. 使用ijkplayer取流播放

demo很简单这里就不放了,可以看一下下面的Java文件和xml布局文件,布局很low,毕竟是简单实现,嫌弃就不要看了

具体实现:

MainActivity.java

public class MainActivity extends Activity implements SrsEncodeHandler.SrsEncodeListener, RtmpHandler.RtmpListener, SrsRecordHandler.SrsRecordListener, View.OnClickListener {

    //拉流地址
    private static final String URL_VOD = "rtmp://123.57.108.220/live/zhibo/zhibo/1";
    //推流地址
    private static final String rtmpUrl = "rtmp://123.57.108.220:1935/live/zhibo/0";

    @BindView(R.id.player)
    VideoView mPlayer;

    @BindView(R.id.publish)
    Button mPublish;

    private SrsPublisher mYaseaCamera;

    /**
     * 所需的所有权限信息
     */
    private static final String[] NEEDED_PERMISSIONS = new String[]{
            Manifest.permission.INTERNET,
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
    };
    private static final int ACTION_REQUEST_PERMISSIONS = 0x001;

    @SuppressLint("InvalidWakeLockTag")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        if (getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        }
        //屏幕保持常亮
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        //权限检查
        if (!checkPermissions(NEEDED_PERMISSIONS)) {
            ActivityCompat.requestPermissions(MainActivity.this, NEEDED_PERMISSIONS, ACTION_REQUEST_PERMISSIONS);
        }
        //播放视频的方法
        init();
    }

    /**
     * 播放视频
     */
    private void init() {
        // init player
        IjkMediaPlayer.loadLibrariesOnce(null);
        // 没有太大用处
        IjkMediaPlayer.native_profileBegin("libijkplayer.so");
        //设置视频地址
        mPlayer.setUrl(URL_VOD);
        //设置控制器
//        StandardVideoController controller = new StandardVideoController(this);
//        controller.addDefaultControlComponent("视频频道", false);
//        mPlayer.setVideoController(controller);
        //这里不设置控制器
        mPlayer.setVideoController(null);
        //进入全屏
//        mPlayer.startFullScreen();
        //视频画面比例,这里使用的填充视频框模式,但是画面可能变形
        mPlayer.setScreenScaleType(SCREEN_SCALE_MATCH_PARENT);
        //设置解码模式,这里设置的IjkPlayer解码
        mPlayer.setPlayerFactory(IjkPlayerFactory.create());
        //使用IjkPlayer解码
//        mPlayer.setPlayerFactory(IjkPlayerFactory.create());
        //使用ExoPlayer解码
//        mPlayer.setPlayerFactory(ExoMediaPlayerFactory.create());
        //使用MediaPlayer解码
//        mPlayer.setPlayerFactory(AndroidMediaPlayerFactory.create());
        //开始播放,不调用则不自动播放
        mPlayer.start();

        camera();

    }

    /**
     * 暂停播放
     */
    @Override
    protected void onPause() {
        super.onPause();
        mPlayer.pause();
        mYaseaCamera.pauseRecord();
    }

    /**
     * 继续播放
     */
    @Override
    protected void onResume() {
        super.onResume();
        mPlayer.resume();
        mYaseaCamera.resumeRecord();
    }

    /**
     * 释放播放器
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mYaseaCamera.stopPublish();
        mYaseaCamera.stopRecord();
        mPlayer.release();
    }

    /**
     * 按下back键
     */
    @Override
    public void onBackPressed() {
        if (!mPlayer.onBackPressed()) {
            super.onBackPressed();
        }
    }

    private void camera() {
        mYaseaCamera = new SrsPublisher((SrsCameraView) findViewById(R.id.yasea_camera));
        //编码状态回调
        mYaseaCamera.setEncodeHandler(new SrsEncodeHandler(this));
        mYaseaCamera.setRecordHandler(new SrsRecordHandler(this));
        //rtmp推流状态回调
        mYaseaCamera.setRtmpHandler(new RtmpHandler(this));
        //预览分辨率
        mYaseaCamera.setPreviewResolution(1280, 720);
        //推流分辨率
        mYaseaCamera.setOutputResolution(720, 1280);
        //传输率
        mYaseaCamera.setVideoHDMode();
        //开启美颜(其他滤镜效果在MagicFilterType中查看)
        mYaseaCamera.switchCameraFilter(MagicFilterType.BEAUTY);
        //打开摄像头,开始预览(未推流)
        mYaseaCamera.startCamera();
        //硬编码
        mYaseaCamera.switchToSoftEncoder();
        //软编码
//        mYaseaCamera.switchToHardEncoder();

        mPublish.setOnClickListener(this);

    }

    @OnClick(R.id.publish)
    public void onClick(View v) {
        switch (v.getId()) {
            default:
                break;
            case R.id.publish:
                if (mPublish.getText().toString().contentEquals("开始")) {
                    //开始推流
                    mYaseaCamera.startPublish(rtmpUrl);
                    mYaseaCamera.startCamera();
                    mPublish.setText("停止");
                    mPublish.setVisibility(View.GONE);
                }else {
                    mYaseaCamera.stopPublish();
                    mPublish.setText("开始");
                }

                break;
        }
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        mYaseaCamera.stopEncode();
        mYaseaCamera.stopRecord();
        mYaseaCamera.setScreenOrientation(newConfig.orientation);
        if (mPublish.getText().toString().contentEquals("停止")) {
            mYaseaCamera.startEncode();
        }
        mYaseaCamera.startCamera();
    }

    @Override
    public void onRtmpConnecting(String msg) {
        Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRtmpConnected(String msg) {
        Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRtmpVideoStreaming() {

    }

    @Override
    public void onRtmpAudioStreaming() {

    }

    @Override
    public void onRtmpStopped() {
        Toast.makeText(getApplicationContext(), "已停止", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRtmpDisconnected() {
        Toast.makeText(getApplicationContext(), "未连接服务器", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRtmpVideoFpsChanged(double fps) {

    }

    @Override
    public void onRtmpVideoBitrateChanged(double bitrate) {

    }

    @Override
    public void onRtmpAudioBitrateChanged(double bitrate) {

    }

    @Override
    public void onRtmpSocketException(SocketException e) {
        handleException(e);
    }

    @Override
    public void onRtmpIOException(IOException e) {
        handleException(e);
    }

    @Override
    public void onRtmpIllegalArgumentException(IllegalArgumentException e) {
        handleException(e);
    }

    @Override
    public void onRtmpIllegalStateException(IllegalStateException e) {
        handleException(e);
    }

    @Override
    public void onNetworkWeak() {
        Toast.makeText(getApplicationContext(), "网络信号弱", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onNetworkResume() {

    }

    @Override
    public void onEncodeIllegalArgumentException(IllegalArgumentException e) {
        handleException(e);
    }

    private void handleException(Exception e) {
        try {
            Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_SHORT).show();
            mYaseaCamera.stopPublish();
            mYaseaCamera.stopRecord();
            mPublish.setText("开始");
        } catch (Exception e1) {
            //
        }
    }

    @Override
    public void onRecordPause() {
        Toast.makeText(getApplicationContext(), "Record paused", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRecordResume() {
        Toast.makeText(getApplicationContext(), "Record resumed", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRecordStarted(String msg) {
        Toast.makeText(getApplicationContext(), "Recording file: " + msg, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRecordFinished(String msg) {
        Toast.makeText(getApplicationContext(), "MP4 file saved: " + msg, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRecordIllegalArgumentException(IllegalArgumentException e) {
        handleException(e);
    }

    @Override
    public void onRecordIOException(IOException e) {
        handleException(e);
    }

    /**
     * 权限检查
     *
     * @param neededPermissions 需要的权限
     * @return 是否全部被允许
     */
    protected boolean checkPermissions(String[] neededPermissions) {
        if (neededPermissions == null || neededPermissions.length == 0) {
            return true;
        }
        boolean allGranted = true;
        for (String neededPermission : neededPermissions) {
            allGranted &= ContextCompat.checkSelfPermission(this, neededPermission) == PackageManager.PERMISSION_GRANTED;
        }
        return allGranted;
    }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.dueeeke.videoplayer.player.VideoView
            android:id="@+id/player"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:ignore="MissingConstraints" />

    </FrameLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right"
            android:layout_marginTop="20dp"
            android:layout_marginRight="20dp"
            android:orientation="vertical">

            <net.ossrs.yasea.SrsCameraView
                android:id="@+id/yasea_camera"
                android:layout_width ="200dp"
                android:layout_height="200dp"/>

        </LinearLayout>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center">

            <Button
                android:id="@+id/publish"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@color/dkplayer_theme_color"
                android:focusable="true"
                android:focusableInTouchMode="true"
                android:textColor="#fff"
                android:textSize="25sp"
                android:text="开始"/>

        </LinearLayout>

    </FrameLayout>

</FrameLayout>
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐