七牛云直播-Android端播放卡顿问题处理

背景:
七牛直播云主要涉及推流SDK、业务控制SDK、播放SDK、转发平台;而在播放端经常会遇到卡断不能播放的问题,此时可能有两种情况,第一,推流端停止推流,即主播下线;第二,播放端网络慢的原因;所以针对第二种情况就需要做一定的处理;

思路:
因为当申请的直播并没有在推流,或者直播过程中发生网络错误(比如:WiFi 断开),播放器在请求超时或者播放完当前缓冲区中的数据后,会触发onError回调,errorCode: ERROR_CODE_IO_ERROR;而这时需要做两个操作:

  1. 判断网络是否可用;
  2. 查询服务端,获知直播是否结束,如果没有结束,则可以尝试做重连;

    注:如果网络断开或者推流结束都Finish Activity,否则重新连接;

问题解决:
针对前面提出的问题及思路,模拟真实环境来做开发调试,而直播最基本需要三个方面:“推流端”、“服务端”、“播放端”;推流端我使用的是OBS推流软件,把流推到我的直播空间(Hub)中,服务端我写了一个Servlet,来获取当前直播流的状态信息,播放端使用的是Android手机来播放(注:模拟器没办法运行程序)

A.推流端,OBS 调用Camera推流,如下:
七牛云直播-Android端播放卡顿问题处理_第1张图片

使用OBS还需要做一定的设置,如图:
七牛云直播-Android端播放卡顿问题处理_第2张图片

B.服务端,使用Servlet来获取当前流的状态信息,返回给客户端,程序如下:

package com.qiniu.pili;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.pili.Hub;
import com.pili.PiliException;
import com.pili.Stream;
import com.pili.Stream.Status;
import com.qiniu.Credentials;

/**
 * Servlet implementation class PiliServerServlet
 */
@WebServlet("/PiliServerServlet")
public class PiliServerServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    private static final String AK = "mfCLP7AlV77j42DZB697zUClBPGdjli_Av******";
    private static final String SK = "FeULzzI79z1EOsDZ0xsXhhXleNEqqN5qZP******";
    private static final String HUB_NAME = "pilistream";


    /**
     * Default constructor. 
     */
    public PiliServerServlet() {

    }

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        Credentials credentials = new Credentials(AK, SK);
        Hub hub = new Hub(credentials, HUB_NAME);
        String streamId = "z1.pilistream.578c8a7efb16df6266052608";
        Stream stream = null;
        try {
            stream = hub.getStream(streamId);
            Status status = stream.status();
            System.out.println(status.toString());
                     response.getOutputStream().println(status.toString());
        } catch (PiliException e) {
            e.printStackTrace();
        }
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        doGet(request, response);

    }

}

注:服务端在本地,应该是localhost:8080/PiliServlet来访问,但我使用了ngrok,来将服务映射到外网;关于ngrok在下一篇博客中会针对使用做详细的说明讲解。

C.Android播放端,首先实现一个PLMediaPlayer.OnErrorListener监听,当ERROR_CODE为PLMediaPlayer.ERROR_CODE_IO_ERROR时,做“思路”中提出的操作,如图所示:
七牛云直播-Android端播放卡顿问题处理_第3张图片
方法实现:
七牛云直播-Android端播放卡顿问题处理_第4张图片
当流信息获取后,可以对其进行判断,是否正常结束推流,然后通过Handler返回UI线程,做finish 或者 视频重新连接播放的操作,如图:
七牛云直播-Android端播放卡顿问题处理_第5张图片
初始化Handler,如图:
七牛云直播-Android端播放卡顿问题处理_第6张图片

android完整程序如下:

package com.qiniu.admin.pilistream;

import android.app.Activity;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.pili.pldroid.player.AVOptions;
import com.pili.pldroid.player.PLMediaPlayer;
import com.pili.pldroid.player.widget.PLVideoView;
import com.qiniu.admin.pilistream.widget.MediaController;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Created by xuhuanchao on 16/7/15.
 */
public class PlayerActivity extends Activity {

    private static final String TAG = "PlayerActivity";
    private static final int MSG_RECONN_STREAM = 1;
    private static final String STATUS_DIS_CONNECTED = "disconnected";
    private static final String STATUS_CONNECTED = "connected";

    Handler mHandler;

    private MediaController mMediaController;
    private PLVideoView mVideoView;
    private Toast mToast = null;
    private String mVideoPath = null;
    private int mDisplayAspectRatio = PLVideoView.ASPECT_RATIO_FIT_PARENT;
    private boolean mIsActivityPaused = true;

    void initHandler(){

        mHandler = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                switch (msg.what){
                    case MSG_RECONN_STREAM:
                        Bundle b = msg.getData();
                        boolean isComplete = b.getBoolean("isComplete");
                        if (isComplete) {
                            finish();
                        } else {
                            mVideoView.setVideoPath(mVideoPath);
                            mIsActivityPaused = false;
                            mVideoView.start();
                        }
                        break;
                }
                return true;
            }
        });
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_player);

        initHandler();
        mVideoView = (PLVideoView) findViewById(R.id.VideoView);

        View loadingView = findViewById(R.id.LoadingView);
        mVideoView.setBufferingIndicator(loadingView);

        mVideoPath = getIntent().getStringExtra("videoPath");

        AVOptions options = new AVOptions();

        int isLiveStreaming = getIntent().getIntExtra("liveStreaming", 1);
        // the unit of timeout is ms
        options.setInteger(AVOptions.KEY_PREPARE_TIMEOUT, 10 * 1000);
        options.setInteger(AVOptions.KEY_GET_AV_FRAME_TIMEOUT, 10 * 1000);
        // Some optimization with buffering mechanism when be set to 1
        options.setInteger(AVOptions.KEY_LIVE_STREAMING, isLiveStreaming);
        if (isLiveStreaming == 1) {
            options.setInteger(AVOptions.KEY_DELAY_OPTIMIZATION, 1);
        }

        // 1 -> hw codec enable, 0 -> disable [recommended]
        int codec = getIntent().getIntExtra("mediaCodec", 0);
        options.setInteger(AVOptions.KEY_MEDIACODEC, codec);

        // whether start play automatically after prepared, default value is 1
        options.setInteger(AVOptions.KEY_START_ON_PREPARED, 0);

        mVideoView.setAVOptions(options);

        // Set some listeners
        mVideoView.setOnInfoListener(mOnInfoListener);
        mVideoView.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener);
        mVideoView.setOnBufferingUpdateListener(mOnBufferingUpdateListener);
        mVideoView.setOnCompletionListener(mOnCompletionListener);
        mVideoView.setOnSeekCompleteListener(mOnSeekCompleteListener);
        mVideoView.setOnErrorListener(mOnErrorListener);

        mVideoView.setVideoPath(mVideoPath);

        // You can also use a custom `MediaController` widget
        mMediaController = new MediaController(this, false, isLiveStreaming == 1);
        mVideoView.setMediaController(mMediaController);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mIsActivityPaused = false;
        mVideoView.start();
    }

    @Override
    protected void onPause() {
        super.onPause();
        mToast = null;
        mIsActivityPaused = true;
        mVideoView.pause();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mVideoView.stopPlayback();
    }

    public void onClickSwitchScreen(View v) {
        mDisplayAspectRatio = (mDisplayAspectRatio + 1) % 5;
        mVideoView.setDisplayAspectRatio(mDisplayAspectRatio);
        switch (mVideoView.getDisplayAspectRatio()) {
            case PLVideoView.ASPECT_RATIO_ORIGIN:
                showToastTips("Origin mode");
                break;
            case PLVideoView.ASPECT_RATIO_FIT_PARENT:
                showToastTips("Fit parent !");
                break;
            case PLVideoView.ASPECT_RATIO_PAVED_PARENT:
                showToastTips("Paved parent !");
                break;
            case PLVideoView.ASPECT_RATIO_16_9:
                showToastTips("16 : 9 !");
                break;
            case PLVideoView.ASPECT_RATIO_4_3:
                showToastTips("4 : 3 !");
                break;
            default:
                break;
        }
    }

    private PLMediaPlayer.OnInfoListener mOnInfoListener = new PLMediaPlayer.OnInfoListener() {
        @Override
        public boolean onInfo(PLMediaPlayer plMediaPlayer, int what, int extra) {
            Log.d(TAG, "onInfo: " + what + ", " + extra);
            return false;
        }
    };


    public static List> jsonToList(String jsonString) {
        List> list = new ArrayList>();
        try {
            Gson gson = new Gson();
            list = gson.fromJson(jsonString,
                    new TypeToken>>() {
                    }.getType());
        } catch (Exception e) {
            // TODO: handle exception
        }
        return list;
    }

    /**
     * 检查推流端是否已正常完成推流
     * @return
     */
    private void checkStreamIsComplete(String url) {
        boolean isComplete = true;
        String streamStatusInfo = getStreamStatus(url);
        if (null != streamStatusInfo && !"".equals(streamStatusInfo)) {
            Log.d(TAG, streamStatusInfo);
            JSONObject jsonObj = null;
            try {
                jsonObj = new JSONObject(streamStatusInfo);
                String status = String.valueOf(jsonObj.get("status"));
                double bytesPerSecond = Double.valueOf(String.valueOf(jsonObj.get("bytesPerSecond")));
                if (STATUS_CONNECTED.equals(status) && bytesPerSecond > 0) {//每秒传输字节数 > 0, 说明主播继续在推流
                    isComplete = false;
                } else {
                    isComplete = true;
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
        Bundle bundle = new Bundle();
        bundle.putBoolean("isComplete", isComplete);
        Message msg = Message.obtain();
        msg.what = MSG_RECONN_STREAM;
        msg.setData(bundle);
        mHandler.sendMessage(msg);
    }

    public String getStreamStatus(String url) {
        String result = "";
        HttpClient client = new DefaultHttpClient();
        HttpGet httpget = new HttpGet(url);
        HttpResponse response;
        try {
            response = client.execute(httpget);
            HttpEntity entity = response.getEntity();

            if (entity != null) {
                InputStream instream = entity.getContent();
                int l;
                byte[] tmp = new byte[2048];
                while ((l = instream.read(tmp)) != -1) {

                }
                result = new String(tmp);
                Log.i(TAG, result);
            }
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 判断网络是否连接
     * @returnw
     */
    private boolean isNetConnected(Context context) {
        if (context != null) {
            ConnectivityManager mConnectivityManager = (ConnectivityManager) context
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            if (mConnectivityManager == null) {
                return false;
            }
            NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
            if (mNetworkInfo == null || !mNetworkInfo.isAvailable()) {
                return false;
            }
        }
        return true;
    }

    /**
     *
     */
    private void getConnectionStatus() {
        int flag = 0;
        /**如果申请的直播并没有在推流,或者直播过程中发生网络错误(比如:WiFi 断开),
         * 播放器在请求超时或者播放完当前缓冲区中的数据后,会触发onError回调,errorCode: ERROR_CODE_IO_ERROR
         *
         * 如何处理该情况:
         * 1.判断网络是否断开
         * 2.查询业务服务器,获知直播是否结束,如果没有结束,则可以尝试做重连
         *
         * 如果决定做重连,则 onError 回调中,请返回 true,否则会导致触发 onCompletion。
         */
        //1.检查手机网络连接
        boolean isConnected = isNetConnected(PlayerActivity.this);
        if (isConnected) {//网络连接是否断开
            //2.如果连接没问题,就判断推流是否在继续
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String url = "http://9ea19420.ngrok.io/PiliServer/";
                    checkStreamIsComplete(url);
                }
            }).start();
        }
    }

    private PLMediaPlayer.OnErrorListener mOnErrorListener = new PLMediaPlayer.OnErrorListener() {
        @Override
        public boolean onError(PLMediaPlayer plMediaPlayer, int errorCode) {
            Log.e(TAG, "Error happened, errorCode = " + errorCode);
//            errorCode = PLMediaPlayer.ERROR_CODE_IO_ERROR;
            switch (errorCode) {
                case PLMediaPlayer.ERROR_CODE_INVALID_URI:
                    showToastTips("Invalid URL !");
                    break;
                case PLMediaPlayer.ERROR_CODE_404_NOT_FOUND:
                    showToastTips("404 resource not found !");
                    break;
                case PLMediaPlayer.ERROR_CODE_CONNECTION_REFUSED:
                    showToastTips("Connection refused !");
                    break;
                case PLMediaPlayer.ERROR_CODE_CONNECTION_TIMEOUT:
                    showToastTips("Connection timeout !");
                    break;
                case PLMediaPlayer.ERROR_CODE_EMPTY_PLAYLIST:
                    showToastTips("Empty playlist !");
                    break;
                case PLMediaPlayer.ERROR_CODE_STREAM_DISCONNECTED:
                    showToastTips("Stream disconnected !");
                    break;
                case PLMediaPlayer.ERROR_CODE_IO_ERROR:
                    getConnectionStatus();
                    break;
                case PLMediaPlayer.ERROR_CODE_UNAUTHORIZED:
                    showToastTips("Unauthorized Error !");
                    break;
                case PLMediaPlayer.ERROR_CODE_PREPARE_TIMEOUT:
                    showToastTips("Prepare timeout !");
                    break;
                case PLMediaPlayer.ERROR_CODE_READ_FRAME_TIMEOUT:
                    showToastTips("Read frame timeout !");
                    break;
                case PLMediaPlayer.MEDIA_ERROR_UNKNOWN:
                default:
                    showToastTips("unknown error !");
                    break;
            }
            // Todo pls handle the error status here, retry or call finish()
            // If you want to retry, do like this:
            // mVideoView.setVideoPath(mVideoPath);
            // mVideoView.start();
            // Return true means the error has been handled
            // If return false, then `onCompletion` will be called
            return true;
        }
    };

    private PLMediaPlayer.OnCompletionListener mOnCompletionListener = new PLMediaPlayer.OnCompletionListener() {
        @Override
        public void onCompletion(PLMediaPlayer plMediaPlayer) {
            Log.d(TAG, "Play Completed !");
            showToastTips("Play Completed !");
            finish();
        }
    };

    private PLMediaPlayer.OnBufferingUpdateListener mOnBufferingUpdateListener = new PLMediaPlayer.OnBufferingUpdateListener() {
        @Override
        public void onBufferingUpdate(PLMediaPlayer plMediaPlayer, int precent) {
            Log.d(TAG, "onBufferingUpdate: " + precent);
        }
    };

    private PLMediaPlayer.OnSeekCompleteListener mOnSeekCompleteListener = new PLMediaPlayer.OnSeekCompleteListener() {
        @Override
        public void onSeekComplete(PLMediaPlayer plMediaPlayer) {
            Log.d(TAG, "onSeekComplete !");
        }
    };

    private PLMediaPlayer.OnVideoSizeChangedListener mOnVideoSizeChangedListener = new PLMediaPlayer.OnVideoSizeChangedListener() {
        @Override
        public void onVideoSizeChanged(PLMediaPlayer plMediaPlayer, int width, int height) {
            Log.d(TAG, "onVideoSizeChanged: " + width + "," + height);
        }
    };

    private void showToastTips(final String tips) {
        if (mIsActivityPaused) {
            return;
        }
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (mToast != null) {
                    mToast.cancel();
                }
                mToast = Toast.makeText(PlayerActivity.this, tips, Toast.LENGTH_SHORT);
                mToast.show();
            }
        });
    }

}

你可能感兴趣的:(Qiniu)