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

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

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

    注:如果网络断开或者推流结束都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() {@Overridepublic 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;}});}@Overrideprotected 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 msoptions.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 1options.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 1options.setInteger(AVOptions.KEY_START_ON_PREPARED, 0);mVideoView.setAVOptions(options);// Set some listenersmVideoView.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` widgetmMediaController = new MediaController(this, false, isLiveStreaming == 1);mVideoView.setMediaController(mMediaController);}@Overrideprotected void onResume() {super.onResume();mIsActivityPaused = false;mVideoView.start();}@Overrideprotected void onPause() {super.onPause();mToast = null;mIsActivityPaused = true;mVideoView.pause();}@Overrideprotected 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() {@Overridepublic 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() {@Overridepublic void run() {String url = "http://9ea19420.ngrok.io/PiliServer/";checkStreamIsComplete(url);}}).start();}}private PLMediaPlayer.OnErrorListener mOnErrorListener = new PLMediaPlayer.OnErrorListener() {@Overridepublic 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 calledreturn true;}};private PLMediaPlayer.OnCompletionListener mOnCompletionListener = new PLMediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(PLMediaPlayer plMediaPlayer) {Log.d(TAG, "Play Completed !");showToastTips("Play Completed !");finish();}};private PLMediaPlayer.OnBufferingUpdateListener mOnBufferingUpdateListener = new PLMediaPlayer.OnBufferingUpdateListener() {@Overridepublic void onBufferingUpdate(PLMediaPlayer plMediaPlayer, int precent) {Log.d(TAG, "onBufferingUpdate: " + precent);}};private PLMediaPlayer.OnSeekCompleteListener mOnSeekCompleteListener = new PLMediaPlayer.OnSeekCompleteListener() {@Overridepublic void onSeekComplete(PLMediaPlayer plMediaPlayer) {Log.d(TAG, "onSeekComplete !");}};private PLMediaPlayer.OnVideoSizeChangedListener mOnVideoSizeChangedListener = new PLMediaPlayer.OnVideoSizeChangedListener() {@Overridepublic 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() {@Overridepublic void run() {if (mToast != null) {mToast.cancel();}mToast = Toast.makeText(PlayerActivity.this, tips, Toast.LENGTH_SHORT);mToast.show();}});}}

七牛云直播-Android端播放卡顿问题处理相关推荐

  1. 七牛云直播推流php,七牛云直播Android推流端之开速开发

    前言 在我看来,定性为快速开发的文档应该是毫无障碍的,对着敲应该就能直接运行的.可是由于七牛迭代太快了,文档跟不上代码迭代的速度,导致快速开始这部分文档的还没更新,很多被废弃的类.方法还在文档中,导致 ...

  2. php七牛云rtmp直播推流,GitHub - jangocheng/FlutterQiniucloudLivePlugin: Flutter 七牛云直播云 推流/播放 SDK集成...

    flutter_qiniucloud_live_plugin Flutter 七牛云直播云插件,支持IOS.Android客户端 Getting Started 集成七牛云直播云推流.观看等功能 功能 ...

  3. android gif播放卡顿,PhotoView播放gif卡顿

    问题出现场景 引入github著名第三方库PhotoView 在项目的需求中有点击查看大图,查看大图里的组件用的PhotoView,并且宽高设置为match_parent,出现了比较明显的卡顿 与Im ...

  4. 《直播疑难杂症排查》之二:播放卡顿

    ##播放卡顿的表现 播放卡顿的表现总结下来包括但不限于以下这些: 频繁出现缓冲 播放不够流畅,画面一卡一卡的 ##常见播放卡顿问题排查 从代码层面来看,什么是卡顿?其实是指播放器渲染的帧率太低,比如: ...

  5. 《直播疑难杂症排查》:播放卡顿

    原文来自七牛云,感谢原作者. 1.播放卡顿的表现 播放卡顿的表现总结下来包括但不限于以下这些: 频繁出现缓冲 播放不够流畅,画面一卡一卡的 2.常见播放卡顿问题排查 从代码层面来看,什么是卡顿?其实是 ...

  6. 直播疑难杂症排查(2) — 播放卡顿

    本文是 <直播疑难杂症排查>系列的第二篇文章,我们主要分析下如何排查播放卡顿问题. 1. 播放卡顿的表现 播放卡顿的表现总结下来包括但不限于以下这些: 频繁出现缓冲 播放不够流畅,画面一卡 ...

  7. 七牛云直播SDK之推流端快速开发

    前言 七牛云直播SDK迭代快,但是官方文档跟不上迭代速度,导致快速开始这部分文档的还没更新,很多被废弃的类.方法还在文档中,本文是基于2.3.0版本进行的快速开发,由于是第一次接触直播,存在着许多不足 ...

  8. Android音视频学习系列(七) — 从0~1开发一款Android端播放器(支持多协议网络拉流本地文件)

    系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...

  9. kodi android 卡顿,解决KODI v17/16在电视上不能打开4K播放卡顿的问题

    本帖最后由 sziboy 于 2018-4-5 16:16 编辑 接下来,解决4K片源播放卡顿的问题. 以为就可以开开心心看片了是吧?非也,1080P的还好,4K,特别是高码率4K的问题就来了,那个卡 ...

最新文章

  1. cv2.resize
  2. ELK系统之logstash问题:retrying failed action with response code: 429
  3. Gartner:2017年应用和基础设施中间件软件市场收入将突破270亿美元
  4. windows 小技巧搜集(不定期更新)
  5. C++入门课程系列:基础知识篇(1)
  6. date oracle 显示毫秒_Oracle date timestamp 毫秒 - 时间函数总结
  7. Accumulator
  8. 重磅!阿里云发布最新服务等级协议SLA ,多实例可用性升为99.995%
  9. abp angular 和mvc_MVC - abp-angular - 博客园
  10. 转化率高的爆款文案都是如何写出来的?
  11. pythonmatplotlib绘图小提琴_使用seaborn制图(小提琴图)
  12. HDU1573 X问题【扩展欧几里得算法】
  13. 不宜佩带佛像,宜佩带佛号、咒语
  14. 设计模式-05.建造者模式与模板方法模式比较
  15. chrome启动参数
  16. Unity-Xlua
  17. python爬虫小说爬取
  18. 数据库sql语句面试题
  19. 鸿蒙系统敏感应用,鸿蒙系统特性“揭晓”!一次开发灵活使用,生态构建难题被解决?...
  20. html 自动填充缓存,禁止input密码自动填充及浏览器缓存密码账号解决方案

热门文章

  1. 涂鸦智能全功能智慧植物生长系统(测试)
  2. 从宏观到微观,零售行业致胜未来的六个趋势
  3. suse linux 如何修改主机名,就这样轻松在Suse修改主机名
  4. 为什么NMOS管比PMOS管用得多--电子迁移率-宽禁带-半导体材料参数介绍
  5. 获取当前网页的协议+域名
  6. 软件架构中的架构模式和最佳实践:探索和实践
  7. 24岁那年,有人除甲醛和家电清洗挣到了人生第一个一百万
  8. 系统缓冲区太小,参见长文本
  9. 深度学习学习笔记(一):深度学习在图像和视频的应用
  10. 经典短篇:叫花子见佛祖