背景:
七牛直播云主要涉及推流SDK、业务控制SDK、播放SDK、转发平台;而在播放端经常会遇到卡断不能播放的问题,此时可能有两种情况,第一,推流端停止推流,即主播下线;第二,播放端网络慢的原因;所以针对第二种情况就需要做一定的处理;
思路:
因为当申请的直播并没有在推流,或者直播过程中发生网络错误(比如:WiFi 断开),播放器在请求超时或者播放完当前缓冲区中的数据后,会触发onError回调,errorCode: ERROR_CODE_IO_ERROR;而这时需要做两个操作:
查询服务端,获知直播是否结束,如果没有结束,则可以尝试做重连;
注:如果网络断开或者推流结束都Finish Activity,否则重新连接;
问题解决:
针对前面提出的问题及思路,模拟真实环境来做开发调试,而直播最基本需要三个方面:“推流端”、“服务端”、“播放端”;推流端我使用的是OBS推流软件,把流推到我的直播空间(Hub)中,服务端我写了一个Servlet,来获取当前直播流的状态信息,播放端使用的是Android手机来播放(注:模拟器没办法运行程序);
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时,做“思路”中提出的操作,如图所示:
方法实现:
当流信息获取后,可以对其进行判断,是否正常结束推流,然后通过Handler返回UI线程,做finish 或者 视频重新连接播放的操作,如图:
初始化Handler,如图:
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