序言

最近在研究直播的弹幕,东西有点多,准备记录一下免得自己忘了又要重新研究,也帮助有这方面需要的同学少走点弯路。关于直播的技术细节其实就是两个方面一个是推流一个是拉流,而弹幕的实现核心在即时聊天,使用聊天室的就能实现,只是消息的展示方式不同而已。在大多数的项目中还是使用第三方的直播平台实现推流功能,因此关于直播平台的选择也是至关重要。下面由我娓娓道来。

效果

为了演示方便我把屏幕录像上传到优酷了,这是视频地址

功能

1.缓冲进度

2.弹幕

3.横竖屏切换

实现

1.直播SDK的选择

提供直播功能的厂商有很多,比如七牛云,乐视,百度云,腾讯云,金山云,等等。功能也大同小异,常见的缩略图,视频录制,转码,都可以实现。但是对于SDK的易用程度还是不敢恭维的。下面我说说我遇到的一些问题。

1.乐视

乐视云 移动直播

优点:
乐视直播的注册流程还是很方便的,选择个人开发者,然后验证身份信息就可以使用了,每人每月免费10GB的流量。

缺点

最大的缺点就是稳定性,至少在我测试的时候也就是2016年9月份稳定性很差,不是说视频的稳定性,而是推流的稳定性,我有一台在同样的网络下我的ViVO X7能推流,但是魅蓝NOTE2不能推流。然而ViVO X7推出去的流在电脑上用VLC能播放,在其他手机上显示黑屏,既不报错也没画面。随后使用同样的网络,同样的魅蓝NOTE2,百度的SDK就能推流。看来乐视的直播技术方面还有待改进,直接pass。

2.七牛云

七牛云官网

优点
态度好,服务周到,其他方面的不能再评价了,因为没有真正使用过,这的确很尴尬,不过态度的确很好,会有客服打电话过来询问需求,会有技术支持人员主动沟通,这是很值得肯定的。

缺点
倒不能算是缺点,可能算特点吧,七牛云需要使用域名别名解析来做RTMP直播流域名,也就是说你要使用七牛云必须要有一个备案过的域名,由于我司的域名我不能轻易去改,而且我也没有备案过的域名,所以不能测试。

3.腾讯云

还没有通过审核,效率太低。

4.阿里云

也需要域名,跳过。

5.百度云

百度音视频直播 LSS

优点

审核速度挺快的,实名认证大概15分钟搞定(这是我的速度,仅供参考),不需要域名,为个人开发者免费提供10G流量测试,这点很良心。而且功能很全面,推流很简单。下面是价格表:

缺点

企业用户需要认证,否则单月最大流量为1TB,个人用户总流量限制在1000GB。

经过以上对比最终选择了百度云来实现直播。

2.及时聊天SDK的选择

这里边倒没有太多的考虑,环信,融云,LeanCloud都可以,但是长期使用leancloud发现其文档质量很高,SDK简单易用。所以使用了LeanCloud来实现即时通讯。

LearnCloud Android 实时通信开发指南

3.弹幕实现

弹幕说白了就是聊天室,只是聊天室的消息需要在视频节目上显示而已,所以首先要实现一个聊天室,此处使用LeanCloud实现。

第一步:初始化

第二步:登录

package com.zgh.livedemo;import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;import com.avos.avoscloud.im.v2.AVIMClient;
import com.avos.avoscloud.im.v2.AVIMException;
import com.avos.avoscloud.im.v2.callback.AVIMClientCallback;public class LoginActivity extends AppCompatActivity {EditText et_name;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_login);et_name = (EditText) findViewById(R.id.et_name);findViewById(R.id.btn_login).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {String name = et_name.getText().toString();if (TextUtils.isEmpty(name)) {Toast.makeText(LoginActivity.this, "登录名不能为空", Toast.LENGTH_SHORT).show();return;}login(name);}});}public void login(String name) {//使用name作为cliendIDAVIMClient jerry = AVIMClient.getInstance(name);jerry.open(new AVIMClientCallback() {@Overridepublic void done(AVIMClient client, AVIMException e) {if (e == null) {Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();//保存clientMyApp.mClient = client;startActivity(new Intent(LoginActivity.this, MainActivity.class));} else {Toast.makeText(LoginActivity.this, "登录失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();}}});}}

第三步,进入聊天室

在进入直播界面的时候调用此方法,进入聊天室。conversationId应该从服务器获取,此处用于测试使用了一个固定的ID。

private void join() {MyApp.mClient.open(new AVIMClientCallback() {@Overridepublic void done(AVIMClient client, AVIMException e) {if (e == null) {//登录成功conv = client.getConversation("57d8b2445bbb50005e420535");conv.join(new AVIMConversationCallback() {@Overridepublic void done(AVIMException e) {if (e == null) {//加入成功Toast.makeText(MainActivity.this, "加入聊天室成功", Toast.LENGTH_SHORT).show();et_send.setEnabled(true);} else {Toast.makeText(MainActivity.this, "加入聊天室失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();et_send.setEnabled(false);android.util.Log.i("zzz", "加入聊天室失败 :" + e.getMessage());}}});}}});}

登录成功以后,在onResum的时候将此Activity注册为消息处理者,在onPause的时候取消注册。而在application的onCreate的时候注册一个默认的处理器,也就是说当APP在后头运行的时候,通过默认处理器处理消息,即弹出状态栏弹出通知,而在聊天界面由当前界面处理消息。

@Overrideprotected void onResume() {super.onResume();AVIMMessageManager.registerMessageHandler(AVIMTextMessage.class, roomMessageHandler);}@Overrideprotected void onPause() {super.onPause();AVIMMessageManager.unregisterMessageHandler(AVIMTextMessage.class, roomMessageHandler);}

在接收到消息以后把消息显示在弹幕控件上。

public class RoomMessageHandler extends AVIMMessageHandler {//接收到消息后的处理逻辑@Overridepublic void onMessage(AVIMMessage message, AVIMConversation conversation, AVIMClient client) {if (message instanceof AVIMTextMessage) {String info = ((AVIMTextMessage) message).getText();//添加消息到屏幕addMsg(info);}}}private void addMsg(String msg) {TextView textView = new TextView(MainActivity.this);textView.setText(msg);ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);params.setMargins(5, 10, 5, 10);textView.setLayoutParams(params);ll_room.addView(textView, 0);barrageView.addMessage(msg);}

弹幕的控件

package com.zgh.livedemo.view;import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Message;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.RelativeLayout;
import android.widget.TextView;import java.util.ArrayList;
import java.util.List;
import java.util.Random;/*** Created by lixueyong on 16/2/19.*/
public class BarrageView extends RelativeLayout {private Context mContext;private BarrageHandler mHandler = new BarrageHandler();private Random random = new Random(System.currentTimeMillis());private static final long BARRAGE_GAP_MIN_DURATION = 1000;//两个弹幕的最小间隔时间private static final long BARRAGE_GAP_MAX_DURATION = 2000;//两个弹幕的最大间隔时间private int maxSpeed = 10000;//速度,msprivate int minSpeed = 5000;//速度,msprivate int maxSize = 30;//文字大小,dpprivate int minSize = 15;//文字大小,dpprivate int totalHeight = 0;private int lineHeight = 0;//每一行弹幕的高度private int totalLine = 0;//弹幕的行数private List<String> messageList = new ArrayList<>();// private String[] itemText = {"是否需要帮忙", "what are you 弄啥来", "哈哈哈哈哈哈哈", "抢占沙发。。。。。。", "************", "是否需要帮忙",//        "我不会轻易的狗带", "嘿嘿", "这是我见过的最长长长长长长长长长长长的评论"};private int textCount;
//    private List<BarrageItem> itemList = new ArrayList<BarrageItem>();public BarrageView(Context context) {this(context, null);}public BarrageView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public BarrageView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);mContext = context;init();}private void init() {// textCount = itemText.length;int duration = (int) ((BARRAGE_GAP_MAX_DURATION - BARRAGE_GAP_MIN_DURATION) * Math.random());mHandler.sendEmptyMessageDelayed(0, duration);}public void addMessage(String message) {messageList.add(message);}@Overridepublic void onWindowFocusChanged(boolean hasWindowFocus) {super.onWindowFocusChanged(hasWindowFocus);totalHeight = getMeasuredHeight();lineHeight = getLineHeight();totalLine = totalHeight / lineHeight;}private void generateItem() {if (messageList.size() > 0) {BarrageItem item = new BarrageItem();String tx = messageList.remove(0);int sz = (int) (minSize + (maxSize - minSize) * Math.random());item.textView = new TextView(mContext);item.textView.setText(tx);item.textView.setTextSize(sz);item.textView.setTextColor(Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256)));item.textMeasuredWidth = (int) getTextWidth(item, tx, sz);item.moveSpeed = (int) (minSpeed + (maxSpeed - minSpeed) * Math.random());if (totalLine == 0) {totalHeight = getMeasuredHeight();lineHeight = getLineHeight();totalLine = totalHeight / lineHeight;}item.verticalPos = random.nextInt(totalLine) * lineHeight;
//        itemList.add(item);showBarrageItem(item);}}private void showBarrageItem(final BarrageItem item) {int leftMargin = this.getRight() - this.getLeft() - this.getPaddingLeft();LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);params.addRule(RelativeLayout.ALIGN_PARENT_TOP);params.topMargin = item.verticalPos;this.addView(item.textView, params);Animation anim = generateTranslateAnim(item, leftMargin);anim.setAnimationListener(new Animation.AnimationListener() {@Overridepublic void onAnimationStart(Animation animation) {}@Overridepublic void onAnimationEnd(Animation animation) {item.textView.clearAnimation();BarrageView.this.removeView(item.textView);}@Overridepublic void onAnimationRepeat(Animation animation) {}});item.textView.startAnimation(anim);}private TranslateAnimation generateTranslateAnim(BarrageItem item, int leftMargin) {TranslateAnimation anim = new TranslateAnimation(leftMargin, -item.textMeasuredWidth, 0, 0);anim.setDuration(item.moveSpeed);anim.setInterpolator(new AccelerateDecelerateInterpolator());anim.setFillAfter(true);return anim;}/*** 计算TextView中字符串的长度** @param text 要计算的字符串* @param Size 字体大小* @return TextView中字符串的长度*/public float getTextWidth(BarrageItem item, String text, float Size) {Rect bounds = new Rect();TextPaint paint;paint = item.textView.getPaint();paint.getTextBounds(text, 0, text.length(), bounds);return bounds.width();}/*** 获得每一行弹幕的最大高度** @return*/private int getLineHeight() {/*  BarrageItem item = new BarrageItem();String tx = itemText[0];item.textView = new TextView(mContext);item.textView.setText(tx);item.textView.setTextSize(maxSize);Rect bounds = new Rect();TextPaint paint;paint = item.textView.getPaint();paint.getTextBounds(tx, 0, tx.length(), bounds);return bounds.height();*/return 50;}class BarrageHandler extends Handler {@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);generateItem();//每个弹幕产生的间隔时间随机int duration = (int) ((BARRAGE_GAP_MAX_DURATION - BARRAGE_GAP_MIN_DURATION) * Math.random());this.sendEmptyMessageDelayed(0, duration);}}}

剩下的细节看demo吧。

4.视频播放

视频的播放使用的是vitamio框架关于具体的API请参考这里这里

需要注意的是在状态的获取,通过设置不同的监听来实现的。

     mVideoView.setOnInfoListener(new MediaPlayer.OnInfoListener() {public boolean onInfo(MediaPlayer mp, int what, int extra) {//缓冲开始if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {layout_loading.setVisibility(View.VISIBLE);android.util.Log.i("zzz", "onStart");//缓冲结束} else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {//此接口每次回调完START就回调END,若不加上判断就会出现缓冲图标一闪一闪的卡顿现象android.util.Log.i("zzz", "onEnd");layout_loading.setVisibility(View.GONE);//   mp.start();mVideoView.start();}return true;}});//获取缓存百分比mVideoView.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {@Overridepublic void onBufferingUpdate(MediaPlayer mp, int percent) {if(!mp.isPlaying()) {layout_loading.setVisibility(View.VISIBLE);tv_present.setText("正在缓冲" + percent + "%");}else{layout_loading.setVisibility(View.GONE);}}});mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {@Overridepublic void onPrepared(MediaPlayer mediaPlayer) {mediaPlayer.setPlaybackSpeed(1.0f);}});//出错处理mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {@Overridepublic boolean onError(MediaPlayer mp, int what, int extra) {tv_present.setText("加载失败");return true;}});

还有就是MediaController的使用,可以参考农民伯伯的vitamio中文API

需要注意的是在xml中使用MediaController时需要这样使用位置为VideoView之上,高度为需要显示的控制条的高度,内部需要包括控制控件,id必须为指定的ID,布局可以参考源码中这个文件

  <io.vov.vitamio.widget.MediaControllerandroid:id="@+id/mediacontroller"android:layout_width="match_parent"android:layout_height="40dp"android:layout_alignParentBottom="true"android:background="#ff0000"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><ImageButtonandroid:id="@+id/mediacontroller_play_pause"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerVertical="true"android:layout_marginLeft="5dp"android:background="@drawable/mediacontroller_button"android:contentDescription="@string/mediacontroller_play_pause"android:src="@drawable/mediacontroller_pause" /></RelativeLayout></io.vov.vitamio.widget.MediaController>

5.视频的全屏模式

其核心的逻辑是点击按钮,改变屏幕方向,在改变方向的时候隐藏聊天室,输入框等。同时改变控件的大小。要让Activity在屏幕切换的时候不重新创建需要添加这个选项。

  android:configChanges="keyboardHidden|orientation|screenSize"

核心代码

 private void fullScreen() {if (isScreenOriatationPortrait(this)) {// 当屏幕是竖屏时full(true);// 点击后变横屏setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);// 设置当前activity为横屏// 当横屏时 把除了视频以外的都隐藏//隐藏其他组件的代码ll_room.setVisibility(View.GONE);et_send.setVisibility(View.GONE);int width=getResources().getDisplayMetrics().widthPixels;int height=getResources().getDisplayMetrics().heightPixels;layout_video.setLayoutParams(new LinearLayout.LayoutParams(height, width));mVideoView.setLayoutParams(new RelativeLayout.LayoutParams(height,width));} else {full(false);setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);// 设置当前activity为竖屏//显示其他组件ll_room.setVisibility(View.VISIBLE);et_send.setVisibility(View.VISIBLE);int width=getResources().getDisplayMetrics().heightPixels;int height= (int) (width*9.0/16);layout_video.setLayoutParams(new LinearLayout.LayoutParams(width, height));mVideoView.setLayoutParams(new RelativeLayout.LayoutParams(width,height));}}//动态隐藏状态栏private void full(boolean enable) {if (enable) {WindowManager.LayoutParams lp = getWindow().getAttributes();lp.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;getWindow().setAttributes(lp);getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);} else {WindowManager.LayoutParams attr = getWindow().getAttributes();attr.flags &= (~WindowManager.LayoutParams.FLAG_FULLSCREEN);getWindow().setAttributes(attr);getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);}}

Demo

关于demo中的配置信息,我抽取到相关的config接口中了,大家只需要配置好就行了

下载地址

package com.zgh.livedemo;/*** Created by zhuguohui on 2016/9/20.*/
public interface Config {/*** learnCloud APP_ID*/String APP_ID = "";/*** learnCloud APP_KEY*/String APP_KEY = "";/*** learnCloud 聊天室ID*/String CONVERSATION_ID = "";/*** rtmp 视频地址*/String VIDEO_URL = "";
}

关于推流用的是百度直播SDK的官方的Demo

一步一步实现直播和弹幕相关推荐

  1. html怎么跟微信公众号关联,微信视频号怎么关联公众号?一步一步教你!

    微信视频号怎么关联公众号?一步一步教你! 刘佳慧 2021-03-30 14:57:30    希财网 微信视频号功能的上线,为很多商家提供了新的带货渠道,可以利用微信平台的流量将商品放在视频号的直播 ...

  2. 一步一步建立10万IP电影网站

    一步一步建立10万IP电影网站 版本号 修改内容 作者 修订时间 V1.0 初始化文档内容 小优 2012-12-10 V1.1 新增网站运营内容 ivan 2013-1-10 V1.2 新增网站推广 ...

  3. 如何一步一步建立自己的技术影响力

    很多朋友是不是有这样的经历,当我们还是一个菜鸟的时候,团队里总有1-2个技术大拿,每当有技术问题大家争论不休,团队成员讨论的面红耳赤,技术大牛实在看不下去,掐灭手中的烟头,喝掉面前方便面的最后一点热汤 ...

  4. 调试JDK源码-一步一步看HashMap怎么Hash和扩容

    调试JDK源码-一步一步看HashMap怎么Hash和扩容 调试JDK源码-ConcurrentHashMap实现原理 调试JDK源码-HashSet实现原理 调试JDK源码-调试JDK源码-Hash ...

  5. 一步一步指引你在Windows7上配置编译使用Caffe(https://github.com/fengbingchun/Caffe_Test)

    之前写过几篇关于Caffe源码在Windows764位上配置编译及使用过程,只是没有把整个工程放到网上,最近把整个工程整理清理了下,把它放到了GitHub上.下面对这个工程的使用作几点说明: 1.   ...

  6. 一步一步实现扫雷游戏(C语言实现)(三)

    使用WIN32API连接窗口 此项目相关博文链接 一步一步实现扫雷游戏(C语言实现)(一) 一步一步实现扫雷游戏(C语言实现)(二) 一步一步实现扫雷游戏(C语言实现)(三) 一步一步实现扫雷游戏(C ...

  7. 一步一步学Silverlight 2系列(3):界面布局

    概述 Silverlight 2 Beta 1版本发布了,无论从Runtime还是Tools都给我们带来了很多的惊喜,如支持框架语言Visual Basic, Visual C#, IronRuby, ...

  8. 一步一步写算法(之图结构)

    原文:一步一步写算法(之图结构) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 图是数据结构里面的重要一章.通过图,我们可以判断两个点之间是 ...

  9. 融合应用11.1.8安装,一步一步的引导

    融合应用11.1.8安装,一步一步的引导 融合应用11.1.8 安装并不是简单的与电子商务套件11 i / R12安装. 所以我们需要安装划分为许多步骤. 请注意,11.1.8 11.1.7总统发布供 ...

最新文章

  1. 一周焦点 | 最强AI芯片麒麟980发布;前端开发者将被取代?
  2. C语言-二维数组与指针
  3. 【机器学习】6大监督学习模型:毒蘑菇分类
  4. Centos 7 docker 拉取镜像慢
  5. (小费马定理降幂)Sum
  6. linux中vi模式中c命令,“Linux”系统中“vi ^C ”命令是什么意思?
  7. Android性能优化 ---(6)自启动管理
  8. 运行MYSQL数据库命令时connetion Timeout expired异常问题
  9. J2EE (十) Java中多种方式实现单例模式
  10. SpringMVC:学习笔记(2)——RequestMapping及请求映射
  11. 制造业ERP项目设计摘录
  12. Eclipse- 使用记录(1)
  13. 因为分区表已变,使用再生龙恢复ubuntu系统后无法休眠的解决办法
  14. 软件jmeter压力测试实例,jmeter压力测试案例实战
  15. ADMM算法理论与应用
  16. C# 实现解答数独功能
  17. 电脑出现ntldr is missing无法开机怎么办
  18. 用python爬取阳光电影的链接
  19. 使用cmd(命令提示符)打开文件磁盘或者文件夹
  20. 搭载“鸿蒙”的华为Watch 3,是智能手表的标准答案吗?

热门文章

  1. 乐鑫 ESP-Touch Sensor 智能触控方案
  2. 鲸鱼算法(WOA)优化支持向量机的数据回归预测,WOA-SVM回归预测,多输入单输出模型。
  3. 继K1之后浪潮发布M13,这一次瞄向了大型关键业务应用
  4. 购房还清款额月份计算
  5. Windows 10微软 正版系统下载
  6. 查看linux系统账号信息,Linux系统查看账户及登录信息的11个方法
  7. Bezier贝塞尔曲线
  8. 简易掉电触发电路的试验
  9. 3GPP相关5G-NR物理层协议
  10. 顶尖 GPU 渲染器 Octane的最新版本