寫在前面的話
前面一篇文章已經(jīng)對(duì)移動(dòng)端數(shù)據(jù)源采集與編碼進(jìn)行了說明,接下來就是將之前采集的數(shù)據(jù)上傳給我們的視頻服務(wù)器了献幔,通過視頻服務(wù)器的轉(zhuǎn)發(fā),可以在web端,app端觀看我們采集的數(shù)據(jù)涝影,從而實(shí)現(xiàn)直播效果,對(duì)于上傳直播數(shù)據(jù)争占,我們一般采用RTMP推流方式燃逻,那么首先我們要了解一下RTMP協(xié)議。
一.RTMP協(xié)議
RTMP協(xié)議是Real Time Message Protocol(實(shí)時(shí)信息傳輸協(xié)議)的縮寫臂痕,它是由Adobe公司提出的一種應(yīng)用層的協(xié)議伯襟,用來解決多媒體數(shù)據(jù)傳輸流的多路復(fù)用(Multiplexing)和分包(packetizing)的問題。
1.簡要介紹
RTMP協(xié)議是應(yīng)用層協(xié)議握童,是要靠底層可靠的傳輸層協(xié)議(通常是TCP)來保證信息傳輸?shù)目煽啃缘哪饭帧T诨趥鬏攲訁f(xié)議的鏈接建立完成后,一個(gè)RTMP協(xié)議的流媒體推流需要經(jīng)過以下幾個(gè)步驟:握手澡绩,建立連接稽揭,建立流,推流肥卡。RTMP連接都是以握手作為開始的溪掀。建立連接階段用于建立客戶端與服務(wù)器之間的“網(wǎng)絡(luò)連接”;建立流階段用于建立客戶端與服務(wù)器之間的“網(wǎng)絡(luò)流”步鉴;推流階段用于傳輸視音頻數(shù)據(jù)揪胃。
接下來就簡單介紹下這一過程
2.握手
在rtmp連接建立后,服務(wù)端與客戶端需要通過3次交換報(bào)文完成握手,握手其他的協(xié)議不同,是由三個(gè)靜態(tài)大小的塊,而不是可變大小的塊組成的,客戶端與服務(wù)器發(fā)送相同的三個(gè)chunk,客戶端發(fā)送c0,c1,c2,服務(wù)端發(fā)送s0,s1,s2。
發(fā)送規(guī)則
- 握手開始于客戶端發(fā)送 C0氛琢,C1 塊喊递。
- 在發(fā)送 C2 之前客戶端必須等待接收 S1 。
- 在發(fā)送任何數(shù)據(jù)之前客戶端必須等待接收 S2阳似。
- 服務(wù)端在發(fā)送 S0 和 S1 之前必須等待接收 C0骚勘,也可以等待接收 C1。
- 服務(wù)端在發(fā)送 S2 之前必須等待接收 C1障般。
- 服務(wù)端在發(fā)送任何數(shù)據(jù)之前必須等待接收 C2调鲸。
數(shù)據(jù)格式
C0與S0
C0和S0的長度是一個(gè)字節(jié),在 S0 中這個(gè)字段表示服務(wù)器選擇的 RTMP 版本挽荡。rtmp1.0規(guī)范所定義的版本是 3藐石;0-2 是早期產(chǎn)品所用的,已被丟棄定拟;4-31保留在未來使用于微;32-255 不允許使用(為了區(qū)分其他以某一字符開始的文本協(xié)議)逗嫡。如果服務(wù)無法識(shí)別客戶端請(qǐng)求的版本,應(yīng)該返回 3 株依∏ぃ客戶端可以選擇減到版本 3 或選擇取消握手。
C1與S1
C1 和 S1 有 1536 字節(jié)長恋腕,由下列字段組成:
時(shí)間:4 字節(jié) 本字段包含時(shí)間戳抹锄。該時(shí)間戳應(yīng)該是發(fā)送這個(gè)數(shù)據(jù)塊的端點(diǎn)的后續(xù)塊的時(shí)間起始點(diǎn)≤伲可以是 0伙单,* 或其他的 任何值。為了同步多個(gè)流哈肖,端點(diǎn)可能發(fā)送其塊流的當(dāng)前值吻育。
零:4 字節(jié) 本字段必須是全零。
隨機(jī)數(shù)據(jù):1528 字節(jié)淤井。 本字段可以包含任何值布疼。 因?yàn)槊總€(gè)端點(diǎn)必須用自己初始化的握手和對(duì)端初始化的握 手來區(qū)分身份,所以這個(gè)數(shù)據(jù)應(yīng)有充分的隨機(jī)性币狠。但是并不需要加密安全的隨機(jī)值游两,或者動(dòng)態(tài)值
C2與S2
C2 和 S2 消息有 1536 字節(jié)長。只是 S1 和 C1 的回復(fù)总寻。本消息由下列字段組成器罐。
時(shí)間:4 字節(jié) 本字段必須包含對(duì)等段發(fā)送的時(shí)間(對(duì) C2 來說是 S1梢为,對(duì) S2 來說是 C1)渐行。
時(shí)間 2:4 字節(jié) 本字段必須包含先前發(fā)送的并被對(duì)端讀取的包的時(shí)間戳。
隨機(jī)回復(fù):1528 字節(jié) 本字段必須包含對(duì)端發(fā)送的隨機(jī)數(shù)據(jù)字段(對(duì) C2 來說是 S1铸董,對(duì) S2 來說是 C1) 祟印。 每個(gè)對(duì)等端可以用時(shí)間和時(shí)間 2 字段中的時(shí)間戳來快速地估計(jì)帶寬和延遲。 但這樣做可 能并不實(shí)用粟害。
RTMP握手的這個(gè)過程就是完成了兩件事:1. 校驗(yàn)客戶端和服務(wù)器端RTMP協(xié)議版本號(hào)蕴忆,2. 是發(fā)了一堆數(shù)據(jù),猜想應(yīng)該是測試一下網(wǎng)絡(luò)狀況悲幅,看看有沒有傳錯(cuò)或者不能傳的情況套鹅。
3.建立網(wǎng)絡(luò)連接
- 客戶端發(fā)送命令消息中的“連接”(connect)到服務(wù)器,請(qǐng)求與一個(gè)服務(wù)應(yīng)用實(shí)例建立連接汰具。
- 服務(wù)器接收到連接命令消息后卓鹿,發(fā)送確認(rèn)窗口大小(Window Acknowledgement Size)協(xié)議消息到客戶端,同時(shí)連接到連接命令中提到的應(yīng)用程序留荔。
- 服務(wù)器發(fā)送設(shè)置帶寬()協(xié)議消息到客戶端吟孙。
- 客戶端處理設(shè)置帶寬協(xié)議消息后,發(fā)送確認(rèn)窗口大小(Window Acknowledgement Size)協(xié)議消息到服務(wù)器端。
- 服務(wù)器發(fā)送用戶控制消息中的“流開始”(Stream Begin)消息到客戶端杰妓。
- 服務(wù)器發(fā)送命令消息中的“結(jié)果”(_result)藻治,通知客戶端連接的狀態(tài)。
注意:
這里面的connect 命令消息巷挥,命令里面包含什么東西桩卵,協(xié)議中沒有說,真實(shí)通信中要指定一些編解碼的信息倍宾,這些信息是以AMF格式發(fā)送的, 其中audioCodecs和videoCodecs這兩個(gè)指定音視頻編碼信息的不能少的吸占。
Window Acknowledgement Size 是設(shè)置接收端消息窗口大小,一般是2500000字節(jié)凿宾,即告訴客戶端你在收到我設(shè)置的窗口大小的這么多數(shù)據(jù)之后給我返回一個(gè)ACK消息矾屯,告訴我你收到了這么多消息。在實(shí)際做推流的時(shí)候推流端要接收很少的服務(wù)器數(shù)據(jù)初厚,遠(yuǎn)遠(yuǎn)到達(dá)不了窗口大小件蚕,所以基本不用考慮這點(diǎn)。而對(duì)于服務(wù)器返回的ACK消息一般也不做處理产禾,我們默認(rèn)服務(wù)器都已經(jīng)收到了這么多消息排作。
服務(wù)器返回的_result命令類型消息的payload length一般不會(huì)大于128字節(jié),但是在最新的nginx-rtmp中返回的消息長度會(huì)大于128字節(jié)亚情,所以一定要做好收包妄痪,組包的工作。
4.建立網(wǎng)絡(luò)流
創(chuàng)建完網(wǎng)絡(luò)連接之后就可以創(chuàng)建網(wǎng)絡(luò)流了
- 客戶端發(fā)送命令消息中releaseStream命令到服務(wù)器端
- 客戶端發(fā)送命令消息中FCPublish命令到服務(wù)器端
- 客戶端發(fā)送命令消息中的“創(chuàng)建流”(createStream)命令到服務(wù)器端楞件。
- 服務(wù)器端接收到“創(chuàng)建流”命令后衫生,發(fā)送命令消息中的“結(jié)果”(_result),通知客戶端流的狀態(tài)土浸。
解析服務(wù)器返回的消息會(huì)得到一個(gè)stream ID, 這個(gè)ID也就是以后和服務(wù)器通信的 message stream ID, 一般返回的是1罪针,不固定。
5.推流命令
推流準(zhǔn)備工作的最后一步是 Publish Stream黄伊,即向服務(wù)器發(fā)一個(gè)publish命令泪酱,這個(gè)命令的message stream ID 就是上面 create stream 之后服務(wù)器返回的stream ID,發(fā)完這個(gè)命令一般不用等待服務(wù)器返回的回應(yīng)还最,直接下一步發(fā)送音視頻數(shù)據(jù)墓阀。有些rtmp庫 還會(huì)發(fā)setMetaData消息,這個(gè)消息可以發(fā)也可以不發(fā)拓轻,里面包含了一些音視頻編碼的信息斯撮。
當(dāng)以上工作都完成的時(shí)候,就可以發(fā)送音視頻了悦即。
二.RTMP推流的實(shí)現(xiàn)流程
前面已經(jīng)介紹了RTMP協(xié)議推流的流程吮成,那么我們?nèi)绾卧贏ndroid上面實(shí)現(xiàn)推流呢橱乱?一般采用FFmpeg來進(jìn)行推流的,我這里采用的是一款純Java的推流庫yasea粱甫,之所以選擇這個(gè)推流第三方庫泳叠,主要是為了了解上述的RTMP協(xié)議推流的流程。
接下來就結(jié)合代碼來分析下實(shí)現(xiàn)推流的功能
1.握手
public void connect(String url) throws IOException {
int port;
String host;
Matcher matcher = rtmpUrlPattern.matcher(url);
if (matcher.matches()) {
tcUrl = url.substring(0, url.lastIndexOf('/'));
swfUrl = "";
pageUrl = "";
host = matcher.group(1);
String portStr = matcher.group(3);
port = portStr != null ? Integer.parseInt(portStr) : 1935;
appName = matcher.group(4);
streamName = matcher.group(6);
} else {
throw new IllegalArgumentException("Invalid RTMP URL. Must be in format: rtmp://host[:port]/application[/streamName]");
}
// socket connection
Log.d(TAG, "connect() called. Host: " + host + ", port: " + port + ", appName: " + appName + ", publishPath: " + streamName);
socket = new Socket();
SocketAddress socketAddress = new InetSocketAddress(host, port);
socket.connect(socketAddress, 3000);
BufferedInputStream in = new BufferedInputStream(socket.getInputStream());
BufferedOutputStream out = new BufferedOutputStream(socket.getOutputStream());
Log.d(TAG, "connect(): socket connection established, doing handhake...");
handshake(in, out);
active = true;
Log.d(TAG, "connect(): handshake done");
rtmpSessionInfo = new RtmpSessionInfo();
readThread = new ReadThread(rtmpSessionInfo, in, this);
writeThread = new WriteThread(rtmpSessionInfo, out, this);
readThread.start();
writeThread.start();
// Start the "main" handling thread
new Thread(new Runnable() {
@Override
public void run() {
try {
Log.d(TAG, "starting main rx handler loop");
handleRxPacketLoop();
} catch (IOException ex) {
Logger.getLogger(RtmpConnection.class.getName()).log(Level.SEVERE, null, ex);
}
}
}).start();
}
private void handshake(InputStream in, OutputStream out) throws IOException {
Handshake handshake = new Handshake();
handshake.writeC0(out);
handshake.writeC1(out); // Write C1 without waiting for S0
out.flush();
handshake.readS0(in);
handshake.readS1(in);
handshake.writeC2(out);
handshake.readS2(in);
}
這里首先匹配我們需要上傳的服務(wù)器地址進(jìn)行匹配茶宵,接下來連接到視頻服務(wù)器危纫,接下來通過handshake方法來進(jìn)行握手協(xié)議,接下來開啟了兩個(gè)線程乌庶,這兩個(gè)線程是用來進(jìn)行讀寫操作的种蝶,讀是讀取服務(wù)器返回的指令,寫是向服務(wù)器發(fā)送指令瞒大,或者音視頻信息螃征,最后開啟一個(gè)線程里面是handleRxPacketLoop方法,這個(gè)方法不斷的讀取服務(wù)器返回的指令透敌。
2.建立網(wǎng)絡(luò)連接
private void rtmpConnect() throws IOException, IllegalStateException {
if (fullyConnected || connecting) {
throw new IllegalStateException("Already connected or connecting to RTMP server");
}
// Mark session timestamp of all chunk stream information on connection.
ChunkStreamInfo.markSessionTimestampTx();
Log.d(TAG, "rtmpConnect(): Building 'connect' invoke packet");
ChunkStreamInfo chunkStreamInfo = rtmpSessionInfo.getChunkStreamInfo(ChunkStreamInfo.RTMP_COMMAND_CHANNEL);
Command invoke = new Command("connect", ++transactionIdCounter, chunkStreamInfo);
invoke.getHeader().setMessageStreamId(0);
AmfObject args = new AmfObject();
args.setProperty("app", appName);
args.setProperty("flashVer", "LNX 11,2,202,233"); // Flash player OS: Linux, version: 11.2.202.233
args.setProperty("swfUrl", swfUrl);
args.setProperty("tcUrl", tcUrl);
args.setProperty("fpad", false);
args.setProperty("capabilities", 239);
args.setProperty("audioCodecs", 3575);
args.setProperty("videoCodecs", 252);
args.setProperty("videoFunction", 1);
args.setProperty("pageUrl", pageUrl);
args.setProperty("objectEncoding", 0);
invoke.addData(args);
writeThread.send(invoke);
connecting = true;
mHandler.onRtmpConnecting("connecting");
}
這里配置了connect命令盯滚,前面也說到這個(gè)命令里面包含了很多東西
接下來就是服務(wù)器的返回信息分別是窗口大小與帶寬信息,這些信息則由前面說的handleRxPacketLoop來讀取并進(jìn)行相關(guān)設(shè)置與反饋
private void handleRxPacketLoop() throws IOException {
// Handle all queued received RTMP packets
while (active) {
while (!rxPacketQueue.isEmpty()) {
RtmpPacket rtmpPacket = rxPacketQueue.poll();
//Log.d(TAG, "handleRxPacketLoop(): RTMP rx packet message type: " + rtmpPacket.getHeader().getMessageType());
switch (rtmpPacket.getHeader().getMessageType()) {
...
case WINDOW_ACKNOWLEDGEMENT_SIZE:
WindowAckSize windowAckSize = (WindowAckSize) rtmpPacket;
int size = windowAckSize.getAcknowledgementWindowSize();
Log.d(TAG, "handleRxPacketLoop(): Setting acknowledgement window size: " + size);
rtmpSessionInfo.setAcknowledgmentWindowSize(size);
// Set socket option
socket.setSendBufferSize(size);
break;
case SET_PEER_BANDWIDTH:
int acknowledgementWindowsize = rtmpSessionInfo.getAcknowledgementWindowSize();
final ChunkStreamInfo chunkStreamInfo = rtmpSessionInfo.getChunkStreamInfo(ChunkStreamInfo.RTMP_CONTROL_CHANNEL);
Log.d(TAG, "handleRxPacketLoop(): Send acknowledgement window size: " + acknowledgementWindowsize);
writeThread.send(new WindowAckSize(acknowledgementWindowsize, chunkStreamInfo));
break;
case COMMAND_AMF0:
handleRxInvoke((Command) rtmpPacket);
break;
default:
Log.w(TAG, "handleRxPacketLoop(): Not handling unimplemented/unknown packet of type: " + rtmpPacket.getHeader().getMessageType());
break;
}
}
// Wait for next received packet
synchronized (rxPacketLock) {
try {
rxPacketLock.wait(500);
} catch (InterruptedException ex) {
Log.w(TAG, "handleRxPacketLoop: Interrupted", ex);
}
}
}
}
這里我們看到收到WINDOW_ACKNOWLEDGEMENT_SIZE這個(gè)命令后,將socket的BufferSize設(shè)置為指定的size了酗电,收到SET_PEER_BANDWIDTH魄藕,writeThread發(fā)送了窗口大小的消息給服務(wù)器了,接下來服務(wù)器就會(huì)返回上面說的結(jié)果命令了撵术,結(jié)果命令的處理為handleRxInvoke方法
private void handleRxInvoke(Command invoke) throws IOException {
String commandName = invoke.getCommandName();
if (commandName.equals("_result")) {
// This is the result of one of the methods invoked by us
String method = rtmpSessionInfo.takeInvokedCommand(invoke.getTransactionId());
Log.d(TAG, "handleRxInvoke: Got result for invoked method: " + method);
if ("connect".equals(method)) {
// Capture server ip/pid/id information if any
String serverInfo = onSrsServerInfo(invoke);
mHandler.onRtmpConnected("connected" + serverInfo);
// We can now send createStream commands
connecting = false;
fullyConnected = true;
synchronized (connectingLock) {
connectingLock.notifyAll();
}
}
...
}
result信息匹配到是connect命令背率,會(huì)進(jìn)行一些參數(shù)的設(shè)置
這里網(wǎng)絡(luò)連接就已經(jīng)建立起來了
3.建立網(wǎng)絡(luò)流
private void createStream() {
if (!fullyConnected) {
throw new IllegalStateException("Not connected to RTMP server");
}
if (currentStreamId != -1) {
throw new IllegalStateException("Current stream object has existed");
}
Log.d(TAG, "createStream(): Sending releaseStream command...");
// transactionId == 2
Command releaseStream = new Command("releaseStream", ++transactionIdCounter);
releaseStream.getHeader().setChunkStreamId(ChunkStreamInfo.RTMP_STREAM_CHANNEL);
releaseStream.addData(new AmfNull()); // command object: null for "createStream"
releaseStream.addData(streamName); // command object: null for "releaseStream"
writeThread.send(releaseStream);
Log.d(TAG, "createStream(): Sending FCPublish command...");
// transactionId == 3
Command FCPublish = new Command("FCPublish", ++transactionIdCounter);
FCPublish.getHeader().setChunkStreamId(ChunkStreamInfo.RTMP_STREAM_CHANNEL);
FCPublish.addData(new AmfNull()); // command object: null for "FCPublish"
FCPublish.addData(streamName);
writeThread.send(FCPublish);
Log.d(TAG, "createStream(): Sending createStream command...");
ChunkStreamInfo chunkStreamInfo = rtmpSessionInfo.getChunkStreamInfo(ChunkStreamInfo.RTMP_COMMAND_CHANNEL);
// transactionId == 4
Command createStream = new Command("createStream", ++transactionIdCounter, chunkStreamInfo);
createStream.addData(new AmfNull()); // command object: null for "createStream"
writeThread.send(createStream);
// Waiting for "NetStream.Publish.Start" response.
synchronized (publishLock) {
try {
publishLock.wait(5000);
} catch (InterruptedException ex) {
// do nothing
}
}
}
這里主要是向服務(wù)器發(fā)送了releaseStream,F(xiàn)CPublish與createStream三個(gè)命令嫩与,服務(wù)器收到這些命令后會(huì)向客戶端返回result命令寝姿,命令中包含后面通訊用的stream ID
private void handleRxInvoke(Command invoke) throws IOException {
String commandName = invoke.getCommandName();
if (commandName.equals("_result")) {
// This is the result of one of the methods invoked by us
String method = rtmpSessionInfo.takeInvokedCommand(invoke.getTransactionId());
Log.d(TAG, "handleRxInvoke: Got result for invoked method: " + method);
...
else if ("createStream".contains(method)) {
// Get stream id
currentStreamId = (int) ((AmfNumber) invoke.getData().get(1)).getValue();
Log.d(TAG, "handleRxInvoke(): Stream ID to publish: " + currentStreamId);
if (streamName != null && publishType != null) {
fmlePublish();
}
}
...
}
可以看到最后調(diào)用了fmlePublish方法,這個(gè)方法是發(fā)送推流命令的
4.推流命令
private void fmlePublish() throws IllegalStateException {
if (!fullyConnected) {
throw new IllegalStateException("Not connected to RTMP server");
}
if (currentStreamId == -1) {
throw new IllegalStateException("No current stream object exists");
}
Log.d(TAG, "fmlePublish(): Sending publish command...");
// transactionId == 0
Command publish = new Command("publish", 0);
publish.getHeader().setChunkStreamId(ChunkStreamInfo.RTMP_STREAM_CHANNEL);
publish.getHeader().setMessageStreamId(currentStreamId);
publish.addData(new AmfNull()); // command object: null for "publish"
publish.addData(streamName);
publish.addData(publishType);
writeThread.send(publish);
}
這里就是向服務(wù)器發(fā)送推流的命令
到這里就是完成了RTMP推流的協(xié)議流程蕴纳,完成后我們就可以將獲取的音視頻推流發(fā)送到視頻服務(wù)器了
如下
@Override
public void publishAudioData(byte[] data) throws IllegalStateException {
if (!fullyConnected) {
throw new IllegalStateException("Not connected to RTMP server");
}
if (currentStreamId == -1) {
throw new IllegalStateException("No current stream object exists");
}
if (!publishPermitted) {
throw new IllegalStateException("Not get the _result(Netstream.Publish.Start)");
}
Audio audio = new Audio();
audio.setData(data);
audio.getHeader().setMessageStreamId(currentStreamId);
writeThread.send(audio);
mHandler.onRtmpAudioStreaming("audio streaming");
}
@Override
public void publishVideoData(byte[] data) throws IllegalStateException {
if (!fullyConnected) {
throw new IllegalStateException("Not connected to RTMP server");
}
if (currentStreamId == -1) {
throw new IllegalStateException("No current stream object exists");
}
if (!publishPermitted) {
throw new IllegalStateException("Not get the _result(Netstream.Publish.Start)");
}
Video video = new Video();
video.setData(data);
video.getHeader().setMessageStreamId(currentStreamId);
writeThread.send(video);
videoFrameCacheNumber.getAndIncrement();
mHandler.onRtmpVideoStreaming("video streaming");
}
由于我們的視頻和音頻是分開推流的会油,那么音視頻同步問題怎么解決呢个粱?
一般來說古毛,視頻同步指的是視頻和音頻同步,也就是說播放的聲音要和當(dāng)前顯示的畫面保持一致都许。想象以下稻薇,看一部電影的時(shí)候只看到人物嘴動(dòng)沒有聲音傳出;或者畫面是激烈的戰(zhàn)斗場景胶征,而聲音不是槍炮聲卻是人物說話的聲音塞椎,這是非常差的一種體驗(yàn)。
在視頻流和音頻流中已包含了其以怎樣的速度播放的相關(guān)數(shù)據(jù)睛低,視頻的幀率(Frame Rate)指示視頻一秒顯示的幀數(shù)(圖像數(shù))案狠;音頻的采樣率(Sample Rate)表示音頻一秒播放的樣本(Sample)的個(gè)數(shù)服傍。可以使用以上數(shù)據(jù)通過簡單的計(jì)算得到其在某一Frame(Sample)的播放時(shí)間骂铁,以這樣的速度音頻和視頻各自播放互不影響吹零,在理想條件下,其應(yīng)該是同步的拉庵,不會(huì)出現(xiàn)偏差灿椅。但,理想條件是什么大家都懂得钞支。如果用上面那種簡單的計(jì)算方式茫蛹,慢慢的就會(huì)出現(xiàn)音視頻不同步的情況。要不是視頻播放快了烁挟,要么是音頻播放快了婴洼,很難準(zhǔn)確的同步。這就需要一種隨著時(shí)間會(huì)線性增長的量撼嗓,視頻和音頻的播放速度都以該量為標(biāo)準(zhǔn)窃蹋,播放快了就減慢播放速度;播放快了就加快播放的速度静稻。所以呢警没,視頻和音頻的同步實(shí)際上是一個(gè)動(dòng)態(tài)的過程,同步是暫時(shí)的振湾,不同步則是常態(tài)杀迹。以選擇的播放速度量為標(biāo)準(zhǔn),快的等待慢的押搪,慢的則加快速度树酪,是一個(gè)你等我趕的過程。
播放速度標(biāo)準(zhǔn)量的的選擇一般來說有以下三種:
- 將視頻同步到音頻上大州,就是以音頻的播放速度為基準(zhǔn)來同步視頻续语。視頻比音頻播放慢了,加快其播放速度厦画;快了疮茄,則延遲播放。
- 將音頻同步到視頻上根暑,就是以視頻的播放速度為基準(zhǔn)來同步音頻力试。
- 將視頻和音頻同步外部的時(shí)鐘上,選擇一個(gè)外部時(shí)鐘為基準(zhǔn)排嫌,視頻和音頻的播放速度都以該時(shí)鐘為標(biāo)準(zhǔn)畸裳。
所以只要我們表示上正確的時(shí)間戳(dts),這樣視頻播放器就會(huì)根據(jù)這個(gè)這個(gè)時(shí)間戳去做音視頻同步
這個(gè)時(shí)間戳是在上傳前添加上去的淳地,時(shí)間可以用系統(tǒng)當(dāng)前時(shí)間怖糊,也可以做其他設(shè)置
yasea中也添加了時(shí)間戳如下
while (!writeQueue.isEmpty()) {
RtmpPacket rtmpPacket = writeQueue.poll();
ChunkStreamInfo chunkStreamInfo = rtmpSessionInfo.getChunkStreamInfo(rtmpPacket.getHeader().getChunkStreamId());
chunkStreamInfo.setPrevHeaderTx(rtmpPacket.getHeader());
rtmpPacket.getHeader().setAbsoluteTimestamp((int) chunkStreamInfo.markAbsoluteTimestampTx());
rtmpPacket.writeTo(out, rtmpSessionInfo.getTxChunkSize(), chunkStreamInfo);
Log.d(TAG, "WriteThread: wrote packet: " + rtmpPacket + ", size: " + rtmpPacket.getHeader().getPacketLength());
if (rtmpPacket instanceof Command) {
rtmpSessionInfo.addInvokedCommand(((Command) rtmpPacket).getTransactionId(), ((Command) rtmpPacket).getCommandName());
}
if (rtmpPacket instanceof Video) {
publisher.getVideoFrameCacheNumber().getAndDecrement();
calcFps();
}
}
out.flush();
到這里就完成了RTMP推流相關(guān)的講解帅容,其實(shí)前面提到的yasea也把關(guān)于直播相關(guān)的內(nèi)容集成進(jìn)去了,雖然不明白為什么一個(gè)推流庫要集成這些東西伍伤。丰嘉。。
寫在后面的話
推流已經(jīng)完成了嚷缭,我們可以通過前面提到的vlc進(jìn)行查看饮亏,但是我們要做的是移動(dòng)端直播,所以下一篇就說一下關(guān)于移動(dòng)端的播放與彈幕評(píng)論相關(guān)的知識(shí)阅爽,peace~~~