视频直播流程以及直播黑屏问题
最近做了一个视频直播的项目,要求在直播不流畅或者网络不可用的时候需要做本地录制,而保证视频不丢失。之前的项目都是只有直播没有本地录制,所以在原来项目上需要做非常多的修改以及代码流程的变更。代码编写工作基本完成之后在做压力测试的时候发现,偶现的直播的时候视频会黑屏。我们的项目是基于开源项目SopCastComponent来进行的,下面的代码分析也是基于此开源项目。视频直播也是基于RTMP协议完成的。
首先说一下基于RTMP协议如何进行一次直播。RTMP协议是基于TCP协议实现的。首先当然是建立TCP链接,TCP链接建立成功之后发送rtmp相关头信息:
private void rtmpConnect() {
SessionInfo.markSessionTimestampTx();
Command invoke = new Command("connect", ++transactionIdCounter);
AmfObject args = new AmfObject();
args.setProperty("app", connectData.appName);
args.setProperty("flashVer", "LNX 11,2,202,233"); // Flash player OS: Linux, version: 11.2.202.233
args.setProperty("swfUrl", connectData.swfUrl);
args.setProperty("tcUrl", connectData.tcUrl);
args.setProperty("fpad", false);
args.setProperty("capabilities", 239);
args.setProperty("audioCodecs", 3575);
args.setProperty("videoCodecs", 252);
args.setProperty("videoFunction", 1);
args.setProperty("pageUrl", connectData.pageUrl);
args.setProperty("objectEncoding", 0);
invoke.addData(args);
MLog.i(TAG,"rtmpConnect---- queuesize = "+((NormalSendQueue)mSendQueue).getBufferFrameCount());
Frame<Chunk> frame = new Frame(invoke, RtmpPacker.CONFIGRATION, Frame.FRAME_TYPE_CONFIGURATION);
mSendQueue.putFrame(frame);
state = State.CONNECTING;
}
这些信息里面包含ip地址,端口号,流名称,flv版本,视频编码格式,音频编码格式等信息。 发送完成之后,服务端根据客户端发送的直播信息,回复响应的命令信息:
private void handleRxCommandInvoke(Command command) {
String commandName = command.getCommandName();
MLog.i(TAG,"handleRxCommandInvoke ");
if(commandName.equals("_result")) {
String method = sessionInfo.takeInvokedCommand(command.getTransactionId());
MLog.d(TAG, "Got result for invoked method: " + method);
if ("connect".equals(method)) {
if(listener != null) {
listener.onRtmpConnectSuccess();
}
createStream();
} else if("createStream".equals(method)) {
currentStreamId = (int) ((AmfNumber) command.getData().get(1)).getValue();
if(listener != null) {
listener.onCreateStreamSuccess();
}
fmlePublish();
}
} else if(commandName.equals("_error")) {
String method = sessionInfo.takeInvokedCommand(command.getTransactionId());
MLog.d(TAG, "Got error for invoked method: " + method);
if ("connect".equals(method)) {
stop();
if(listener != null) {
listener.onRtmpConnectFail();
}
} else if("createStream".equals(method)) {
stop();
if(listener != null) {
listener.onCreateStreamFail();
}
}
} else if(commandName.equals("onStatus")) {
String code = ((AmfString) ((AmfObject) command.getData().get(1)).getProperty("code")).getValue();
if (code.equals("NetStream.Publish.Start")||code.equals("NetStream.Record.Start")) {
MLog.d(TAG, "Got publish start success");
state = State.LIVING;
if(listener != null) {
listener.onPublishSuccess();
}
onMetaData();
// We can now publish AV data
publishPermitted = true;
} else {
MLog.d(TAG, "Got publish start fail");
stop();
if(listener != null) {
listener.onPublishFail();
}
}
} else {
MLog.d(TAG, "Got Command result: " + commandName);
}
}
其中有rmtp建立成功,流创建成功,pushlish等相关命令。到流创建命令成功之后就可以发送直播数据了。
上面指定的视频格式为flv,虽然流创建成功了,但是并没有相关视频的信息,下面就是发送流相关的信息。首先是flv的头信息:
public static void writeFlvHeader(ByteBuffer buffer, boolean hasVideo, boolean hasAudio) {
/**
* Flv Header在当前版本中总是由9个字节组成。
* 第1-3字节为文件标识(Signature),总为“FLV”(0x46 0x4C 0x56),如图中紫色区域。
* 第4字节为版本,目前为1(0x01)。
* 第5个字节的前5位保留,必须为0。
* 第5个字节的第6位表示是否存在音频Tag。
* 第5个字节的第7位保留,必须为0。
* 第5个字节的第8位表示是否存在视频Tag。
* 第6-9个字节为UI32类型的值,表示从File Header开始到File Body开始的字节数,版本1中总为9。
*/
byte[] signature = new byte[] {'F', 'L', 'V'}; /* always "FLV" */
byte version = (byte) 0x01; /* should be 1 */
byte videoFlag = hasVideo ? (byte) 0x01 : 0x00;
byte audioFlag = hasAudio ? (byte) 0x04 : 0x00;
byte flags = (byte) (videoFlag | audioFlag); /* 4, audio; 1, video; 5 audio+video.*/
byte[] offset = new byte[] {(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x09}; /* always 9 */
buffer.put(signature);
buffer.put(version);
buffer.put(flags);
buffer.put(offset);
}
头里面包含了flv的标志,是否有音视频的信息以及版本号等信息。下面就是写入MetaData,这个里面包含了视频的详细信息:
public static byte[] writeFlvMetaData(int width, int height, int fps, int audioRate, int audioSize, boolean isStereo) {
AmfString metaDataHeader = new AmfString("onMetaData", false);
AmfMap amfMap = new AmfMap();
MLog.i("writeFlvMetaData","writeFlvMetaData width = "+width+" height = "+height);
amfMap.setProperty("width", width);
amfMap.setProperty("height", height);
amfMap.setProperty("framerate", fps);
amfMap.setProperty("videocodecid", FlvVideoCodecID.AVC);
amfMap.setProperty("audiosamplerate", audioRate);
amfMap.setProperty("audiosamplesize", audioSize);
if(isStereo) {
amfMap.setProperty("stereo", true);
} else {
amfMap.setProperty("stereo", false);
}
amfMap.setProperty("audiocodecid", FlvAudio.AAC);
int size = amfMap.getSize() + metaDataHeader.getSize();
ByteBuffer amfBuffer = ByteBuffer.allocate(size);
amfBuffer.put(metaDataHeader.getBytes());
amfBuffer.put(amfMap.getBytes());
return amfBuffer.array();
}
这个里面包含了视频的宽高,关键帧频率,视频编码,音频编码等视频的详细信息。再到后面就是写入视频数据了。flv的视频格式又tagsize和tag组成,tag由tagheader和tagboday组成。详细的信息可以在网上搜素。
直播的基本过程讲完了,但是什么原因会导致黑屏呢?我们把相关的直播数据保存下来,发现flv的格式头是有的,matedata也是有的,里面也携带了相关的视频信息。在后面的帧里面也有直播的数据。但是为什么就是黑屏呢?而且把保存下来的直播文件拿到其他播放器中播放,也确实没有画面。反复查看对比正常的直播和异常的直播数据发现,异常的直播数据里面少了一个视频的第一帧信息。因为某种原因导致在直播流还没有开始的时候向直播中写入第一帧信息,而导致第一帧的信息丢失。从而推断,播放器在解析视频的时候查看视频的信息并不是只查看matedata里面的视频信息,还要看视频第一帧的信息,如果视频第一帧的信息丢失,那么就算视频中有其他视频帧视频也不会播放。第一帧代码如下:
//写入Flv Video Header
writeVideoHeader(buffer, FlvVideoFrameType.KeyFrame, FlvVideoCodecID.AVC, FlvVideoAVCPacketType.SequenceHeader);
buffer.put((byte)0x01);
buffer.put(sps[1]);
buffer.put(sps[2]);
buffer.put(sps[3]);
buffer.put((byte)0xff);
buffer.put((byte)0xe1);
buffer.putShort((short)sps.length);
buffer.put(sps);
buffer.put((byte)0x01);
buffer.putShort((short)pps.length);
buffer.put(pps);
}
来源:oschina
链接:https://my.oschina.net/u/1013544/blog/3195196