移动端视频开发通过什么方式实现直播?十分钟带你快速了解
MPEG2-TS 格式 (后缀为 .ts) 目前,我们在流媒体传输,尤其是直播中主要采用的就是 FLV 和 MPEG2-TS 格式,分别用于 RTMP/HTTP-FLV 和 HLS 协议。
#####4.推流到服务器推流是直播的第一公里,直播的推流对这个直播链路影响非常大,如果推流的网络不稳定,无论我们如何做优化,观众的体验都会很糟糕。所以也是我们排查问题的第一步,如何系统地解决这类问题需要我们对相关理论有基础的认识。 推送协议主要有三种:
RTSP(Real Time Streaming Protocol):实时流传送协议,是用来控制声音或影像的多媒体串流协议, 由 Real Networks 和 Netscape 共同提出的;
RTMP(Real Time Messaging Protocol):实时消息传送协议,是 Adobe 公司为 Flash 播放器和服务器之间音频、视频和数据传输 开发的开放协议;
HLS(HTTP Live Streaming):是苹果公司(Apple Inc.)实现的基于 HTTP 的流媒体传输协议;
RTMP 协议基于 TCP,是一种设计用来进行实时数据通信的网络协议,主要用来在 flash/AIR 平台和支持 RTMP 协议的流媒体/交互服务器之间进行音视频和数据通信。支持该协议的软件包括 Adobe Media Server/Ultrant Media Server/red5 等。 它有三种变种:
RTMP 工作在 TCP 之上的明文协议,使用端口 1935;
RTMPT 封装在 HTTP 请求之中,可穿越防火墙;
RTMPS 类似 RTMPT,但使用的是 HTTPS 连接;
RTMP 是目前主流的流媒体传输协议,广泛用于直播领域,可以说市面上绝大多数的直播产品都采用了这个协议。 RTMP 协议就像一个用来装数据包的容器,这些数据可以是 AMF 格式的数据,也可以是 FLV 中的视/音频数据。一个单一的连接可以通过不同的通道传输多路网络流。这些通道中的包都是按照固定大小的包传输的。
#####5.服务器流分发流媒体服务器的作用是负责直播流的发布和转播分发功能。 流媒体服务器有诸多选择,如商业版的 Wowza。但我选择的是 Nginx,它是一款优秀的免费 Web 服务器,后面我会详细介绍如何搭建 Nginx 服务器。#####6.播放器流播放主要是实现直播节目在终端上的展现。因为我这里使用的传输协议是 RTMP, 所以只要支持 RTMP 流协议的播放器都可以使用,譬如:
电脑端:VLC 等
手机端:Vitamio 以及 ijkplayer 等
一般情况下我们把上面流程的前四步称为第一部分,即视频主播端的操作。视频采集处理后推流到流媒体服务器,第一部分功能完成。第二部分就是流媒体服务器,负责把从第一部分接收到的流进行处理并分发给观众。第三部分就是观众啦,只需要拥有支持流传输协议的播放器即可。
###二.第一部分:采集推流 SDK 目前市面上集视频采集、编码、封装和推流于一体的 SDK 已经有很多了,例如商业版的 NodeMedia,但 NodeMedia SDK 按包名授权,未授权包名应用使用有版权提示信息。 我这里使用的是别人分享在 github 上的一个免费 SDK,[下载地址](
)。下面我就代码分析一下直播推流的过程吧: 先看入口界面:
很简单,一个输入框让你填写服务器的推流地址,另外一个按钮开启推流。
public class StartActivity extends Activity {public static final String RTMPURL_MESSAGE = "rtmppush.hx.com.rtmppush.rtmpurl";private Button _startRtmpPushButton = null;private EditText _rtmpUrlEditText = null;
private View.OnClickListener _startRtmpPushOnClickedEvent = new View.OnClickListener() {@Overridepublic void onClick(View arg0) {Intent i = new Intent(StartActivity.this, MainActivity.class);String rtmpUrl = _rtmpUrlEditText.getText().toString();i.putExtra(StartActivity.RTMPURL_MESSAGE, rtmpUrl);StartActivity.this.startActivity(i);}};
private void InitUI(){_rtmpUrlEditText = (EditText)findViewById(R.id.rtmpUrleditText);_startRtmpPushButton = (Button)findViewById(R.id.startRtmpButton);_rtmpUrlEditText.setText("rtmp://192.168.1.104:1935/liv
e/12345");_startRtmpPushButton.setOnClickListener(_startRtmpPushOnClickedEvent);}
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_start);InitUI();}}
主要的推流过程在 MainActivity 里面,同样,先看界面:
布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/cameraRelative"android:layout_width="match_parent"android:layout_height="match_parent"android:paddingBottom="@dimen/activity_vertical_margin"android:paddingLeft="@dimen/activity_horizontal_margin"android:paddingRight="@dimen/activity_horizontal_margin"android:paddingTop="@dimen/activity_vertical_margin"android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
<SurfaceViewandroid:id="@+id/surfaceViewEx"android:layout_width="match_parent"android:layout_height="match_parent"/><Buttonandroid:id="@+id/SwitchCamerabutton"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignBottom="@+id/surfaceViewEx"android:text="@string/SwitchCamera" /></RelativeLayout>
其实就是用一个 SurfaceView 显示摄像头拍摄画面,并提供了一个按钮切换前置和后置摄像头。从入口函数看起:
@Overrideprotected void onCreate(Bundle savedInstanceState) {requestWindowFeature(Window.FEATURE_NO_TITLE);getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
Intent intent = getIntent();_rtmpUrl = intent.getStringExtra(StartActivity.RTMPURL_MESSAGE);
InitAll();
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);_wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag");
首先设置全屏显示,常亮,竖屏,获取服务器的推流 url,再初始化所有东西。
private void InitAll() {WindowManager wm = this.getWindowManager();
int width = wm.getDefaultDisplay().getWidth();int height = wm.getDefaultDisplay().getHeight();int iNewWidth = (int) (height * 3.0 / 4.0);
RelativeLayout rCameraLayout = (RelativeLayout)findViewById(R.id.cameraRelative);RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,RelativeLayout.LayoutParams.MATCH_PARENT);int iPos = width - iNewWidth;layoutParams.setMargins(iPos, 0, 0, 0);
_mSurfaceView = (SurfaceView) this.findViewById(R.id.surfaceViewEx);_mSurfaceView.getHolder().setFixedSize(HEIGHT_DEF, WIDTH_DEF);_mSurfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);_mSurfaceView.getHolder().setKeepScreenOn(true);_mSurfaceView.getHolder().addCallback(new SurceCallBack());_mSurfaceView.setLayoutParams(layoutParams);
InitAudioRecord();
_SwitchCameraBtn = (Button) findViewById(R.id.SwitchCamerabutton);_SwitchCameraBtn.setOnClickListener(_switchCameraOnClickedEvent);
RtmpStartMessage();//开始推流}
首先设置屏幕比例 3:4 显示,给 SurfaceView 设置一些参数并添加回调,再初始化 AudioRecord,最后执行开始推流。音频在这里初始化了,那么相机在哪里初始化呢?其实在 SurfaceView 的回调函数里。
@Overridepublic void surfaceCreated(SurfaceHolder holder) {_iDegrees = getDisplayOritation(getDispalyRotation(), 0);if (_mCamera != null) {InitCamera(); //初始化相机 return;}//华为 i7 前后共用摄像头 if (Camera.getNumberOfCameras() == 1) {_bIsFront = false;_mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);} else {_mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);}InitCamera();}
@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {}}
相机的初始化就在这里啦:
public void InitCamera() {Camera.Parameters p = _mCamera.getParameters();
Size prevewSize = p.getPreviewSize();showlog("Original Width:" + prevewSize.width + ", height:" + prevewSize.height);
List<Size> PreviewSizeList = p.getSupportedPreviewSizes();List<Integer> PreviewFormats = p.getSupportedPreviewFormats();showlog("Listing all supported preview sizes");for (Camera.Size size : PreviewSizeList) {showlog(" w: " + size.width + ", h: " + size.height);}
showlog("Listing all supported preview formats");Integer iNV21Flag = 0;Integer iYV12Flag = 0;for (Integer yuvFormat : PreviewFormats) {showlog("preview formats:" + yuvFormat);if (yuvFormat == android.graphics.ImageFormat.YV12) {iYV12Flag = android.graphics.ImageFormat.YV12;}if (yuvFormat == android.graphics.ImageFormat.NV21) {iNV21Flag = android.graphics.ImageFormat.NV21;}}
if (iNV21Flag != 0) {_iCameraCodecType = iNV21Flag;} else if (iYV12Flag != 0) {_iCameraCodecType = iYV12Flag;}p.setPreviewSize(HEIGHT_DEF, WIDTH_DEF);p.setPreviewFormat(_iCameraCodecType);p.setPreviewFrameRate(FRAMERATE_DEF);
showlog("_iDegrees="+_iDegrees);_mCamera.setDisplayOrientation(_iDegrees);p.setRotation(_iDegrees);_mCamera.setPreviewCallback(_previewCallback);_mCamera.setParameters(p);try {_mCamera.setPreviewDisplay(_mSurfaceView.getHolder());} catch (Exception e) {return;}_mCamera.cancelAutoFocus();//只有加上了这一句,才会自动对焦。_mCamera.startPreview();}
还记得之前初始化完成之后开始推流函数吗?
private void RtmpStartMessage() {Message msg = new Message();msg.what = ID_RTMP_PUSH_START;Bundle b = new Bundle();b.putInt("ret", 0);msg.setData(b);mHandler.sendMessage(msg);}
Handler 处理:
public Handler mHandler = new Handler() {public void handleMessage(android.os.Message msg) {Bundle b = msg.getData();int ret;switch (msg.what) {case ID_RTMP_PUSH_START: {Start();break;}}}};}
真正的推流实现原来在这里:
private void Start() {if (DEBUG_ENABLE) {File saveDir = Environment.getExternalStorageDirectory();String strFilename = saveDir + "/aaa.h264";try {if (!new File(strFilename).exists()) {new File(strFilename).createNewFile();}_outputStream = new DataOutputStream(new FileOutputStream(strFilename));} catch (Exception e) {e.printStackTrace();}}//_rtmpSessionMgr.Start("rtmp://192.168.0.110/live/12345678");_rtmpSessionMgr = new RtmpSessionManager();_rtmpSessionMgr.Start(_rtmpUrl); //------point 1
int iFormat = _iCameraCodecType;_swEncH264 = new SWVideoEncoder(WIDTH_DEF, HEIGHT_DEF, FRAMERATE_DEF, BITRATE_DEF);_swEncH264.start(iFormat); //------point 2
_bStartFlag = true;
_h264EncoderThread = new Thread(_h264Runnable);_h264EncoderThread.setPriority(Thread.MAX_PRIORITY);_h264EncoderThread.start(); //------point 3
_AudioRecorder.startRecording();_AacEncoderThread = new Thread(_aacEncoderRunnable);_AacEncoderThread.setPriority(Thread.MAX_PRIORITY);_AacEncoderThread.start(); //------point 4}
里面主要的函数有四个,我分别标出来了,现在我们逐一看一下。首先是 point 1,这已经走到 SDK 里面了
public int Start(String rtmpUrl){int iRet = 0;
_rtmpUrl = rtmpUrl;_rtmpSession = new RtmpSession();
_bStartFlag = true;_h264EncoderThread.setPriority(Thread.MAX_PRIORITY);_h264EncoderThread.start();
return iRet;}
其实就是启动了一个线程,这个线程稍微有点复杂
private Thread _h264EncoderThread = new Thread(new Runnable() {
private Boolean WaitforReConnect(){for(int i=0; i < 500; i++){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}if(_h264EncoderThread.interrupted() || (!_bStartFlag)){return false;}}return true;}@Overridepublic void run() {while (!_h264EncoderThread.interrupted() && (_bStartFlag)) {if(_rtmpHandle == 0) {_rtmpHandle = _rtmpSession.RtmpConnect(_rtmpUrl);if(_rtmpHandle == 0){if(!WaitforReConnect()){break;}continue;}}else{if(_rtmpSession.RtmpIsConnect(_rtmpHandle) == 0){_rtmpHandle = _rtmpSession.RtmpConnect(_rtmpUrl);if(_rtmpHandle == 0){if(!WaitforReConnect()){break;}continue;}}}
if((_videoDataQueue.size() == 0) && (_audioDataQueue.size()==0)){try {Thread.sleep(30);} catch (InterruptedException e) {e.printStackTrace();}continue;}//Log.i(TAG, "VideoQueue length="+_videoDataQueue.size()+", AudioQueue length="+_audioDataQueue.size());for(int i = 0; i < 100; i++){byte[] audioData = GetAndReleaseAudioQueue();if(audioData == null){break;}//Log.i(TAG, "###RtmpSendAudioData:"+audioData.length);_rtmpSession.RtmpSendAudioData(_rtmpHandle, audioData, audioData.length);}
byte[] videoData = GetAndReleaseVideoQueue();if(videoData != null){//Log.i(TAG, "$$$RtmpSendVideoData:"+videoData.length);_rtmpSession.RtmpSendVideoData(_rtmpHandle, videoData, videoData.length);}try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}_videoDataQueueLock.lock();_videoDataQueue.clear();_videoDataQueueLock.unlock();_audioDataQueueLock.lock();_audioDataQueue.clear();_audioDataQueueLock.unlock();
if((_rtmpHandle != 0) && (_rtmpSession != null)){_rtmpSession.RtmpDisconnect(_rtmpHandle);}_rtmpHandle = 0;_rtmpSession = null;}});
评论