七牛云直播-Android端播放卡顿问题处理
2016-07-22 16:33
696 查看
背景:
七牛直播云主要涉及推流SDK、业务控制SDK、播放SDK、转发平台;而在播放端经常会遇到卡断不能播放的问题,此时可能有两种情况,第一,推流端停止推流,即主播下线;第二,播放端网络慢的原因;所以针对第二种情况就需要做一定的处理;
思路:
因为当申请的直播并没有在推流,或者直播过程中发生网络错误(比如:WiFi 断开),播放器在请求超时或者播放完当前缓冲区中的数据后,会触发onError回调,errorCode: ERROR_CODE_IO_ERROR;而这时需要做两个操作:
判断网络是否可用;
查询服务端,获知直播是否结束,如果没有结束,则可以尝试做重连;
注:如果网络断开或者推流结束都Finish Activity,否则重新连接;
问题解决:
针对前面提出的问题及思路,模拟真实环境来做开发调试,而直播最基本需要三个方面:“推流端”、“服务端”、“播放端”;推流端我使用的是OBS推流软件,把流推到我的直播空间(Hub)中,服务端我写了一个Servlet,来获取当前直播流的状态信息,播放端使用的是Android手机来播放(注:模拟器没办法运行程序);
A.推流端,OBS 调用Camera推流,如下:
使用OBS还需要做一定的设置,如图:
B.服务端,使用Servlet来获取当前流的状态信息,返回给客户端,程序如下:
注:服务端在本地,应该是localhost:8080/PiliServlet来访问,但我使用了ngrok,来将服务映射到外网;关于ngrok在下一篇博客中会针对使用做详细的说明讲解。
C.Android播放端,首先实现一个PLMediaPlayer.OnErrorListener监听,当ERROR_CODE为PLMediaPlayer.ERROR_CODE_IO_ERROR时,做“思路”中提出的操作,如图所示:
方法实现:
当流信息获取后,可以对其进行判断,是否正常结束推流,然后通过Handler返回UI线程,做finish 或者 视频重新连接播放的操作,如图:
初始化Handler,如图:
android完整程序如下:
七牛直播云主要涉及推流SDK、业务控制SDK、播放SDK、转发平台;而在播放端经常会遇到卡断不能播放的问题,此时可能有两种情况,第一,推流端停止推流,即主播下线;第二,播放端网络慢的原因;所以针对第二种情况就需要做一定的处理;
思路:
因为当申请的直播并没有在推流,或者直播过程中发生网络错误(比如:WiFi 断开),播放器在请求超时或者播放完当前缓冲区中的数据后,会触发onError回调,errorCode: ERROR_CODE_IO_ERROR;而这时需要做两个操作:
判断网络是否可用;
查询服务端,获知直播是否结束,如果没有结束,则可以尝试做重连;
注:如果网络断开或者推流结束都Finish Activity,否则重新连接;
问题解决:
针对前面提出的问题及思路,模拟真实环境来做开发调试,而直播最基本需要三个方面:“推流端”、“服务端”、“播放端”;推流端我使用的是OBS推流软件,把流推到我的直播空间(Hub)中,服务端我写了一个Servlet,来获取当前直播流的状态信息,播放端使用的是Android手机来播放(注:模拟器没办法运行程序);
A.推流端,OBS 调用Camera推流,如下:
使用OBS还需要做一定的设置,如图:
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<Map<String, Object>> jsonToList(String jsonString) { List<Map<String, Object>> list = new ArrayList<Map<String, Object>>(); try { Gson gson = new Gson(); list = gson.fromJson(jsonString, new TypeToken<List<Map<String, Object>>>() { }.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(); } }); } }
相关文章推荐
- 使用SpanableString实现textview部分字体点击事件(不同颜色)
- 2016.7.20学习总结,关于屏幕适配
- Android 事件分发
- 删除之前的保存的图片
- Android和Untiy3D的交互方式
- Android Studio出现UnsupportedClassVersionError Unsupported major.minor version 52.0
- 深入理解 Android 中的Matrix
- 从服务器获得图片并保存到手机
- 调用系统级别权限(比如修改时间)
- 经验与教训
- android 事件监听
- Android天气预报app改进版
- Canvas 学习之 (4) rotate
- 使用Nuwa实现Android热修复
- [Android]自定义EmptyView列表数据为空显示
- Android开发介绍
- Android 吸入动画效果详解
- Android程序构成和打包安装
- RecyclerView控件的使用
- android service不被kill的方法