移動(dòng)端直播開(kāi)發(fā)(三)RTMP推流

寫(xiě)在前面的話

前面一篇文章已經(jīng)對(duì)移動(dòng)端數(shù)據(jù)源采集與編碼進(jìn)行了說(shuō)明,接下來(lái)就是將之前采集的數(shù)據(jù)上傳給我們的視頻服務(wù)器了,通過(guò)視頻服務(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é)議)的縮寫(xiě),它是由Adobe公司提出的一種應(yīng)用層的協(xié)議,用來(lái)解決多媒體數(shù)據(jù)傳輸流的多路復(fù)用(Multiplexing)和分包(packetizing)的問(wèn)題。

1.簡(jiǎn)要介紹

RTMP協(xié)議是應(yīng)用層協(xié)議,是要靠底層可靠的傳輸層協(xié)議(通常是TCP)來(lái)保證信息傳輸?shù)目煽啃缘摹T诨趥鬏攲訁f(xié)議的鏈接建立完成后,一個(gè)RTMP協(xié)議的流媒體推流需要經(jīng)過(guò)以下幾個(gè)步驟:握手,建立連接,建立流,推流。RTMP連接都是以握手作為開(kāi)始的。建立連接階段用于建立客戶端與服務(wù)器之間的“網(wǎng)絡(luò)連接”;建立流階段用于建立客戶端與服務(wù)器之間的“網(wǎng)絡(luò)流”;推流階段用于傳輸視音頻數(shù)據(jù)。

接下來(lái)就簡(jiǎn)單介紹下這一過(guò)程

2.握手

在rtmp連接建立后,服務(wù)端與客戶端需要通過(guò)3次交換報(bào)文完成握手,握手其他的協(xié)議不同,是由三個(gè)靜態(tài)大小的塊,而不是可變大小的塊組成的,客戶端與服務(wù)器發(fā)送相同的三個(gè)chunk,客戶端發(fā)送c0,c1,c2,服務(wù)端發(fā)送s0,s1,s2。

發(fā)送規(guī)則

  • 握手開(kāi)始于客戶端發(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的長(zhǎng)度是一個(gè)字節(jié),在 S0 中這個(gè)字段表示服務(wù)器選擇的 RTMP 版本。rtmp1.0規(guī)范所定義的版本是 3;0-2 是早期產(chǎn)品所用的,已被丟棄;4-31保留在未來(lái)使用;32-255 不允許使用(為了區(qū)分其他以某一字符開(kāi)始的文本協(xié)議)。如果服務(wù)無(wú)法識(shí)別客戶端請(qǐng)求的版本,應(yīng)該返回 3 。客戶端可以選擇減到版本 3 或選擇取消握手。

C1與S1
C1 和 S1 有 1536 字節(jié)長(zhǎng),由下列字段組成:
時(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ì)端初始化的握 手來(lái)區(qū)分身份,所以這個(gè)數(shù)據(jù)應(yīng)有充分的隨機(jī)性。但是并不需要加密安全的隨機(jī)值,或者動(dòng)態(tài)值

C2與S2
C2 和 S2 消息有 1536 字節(jié)長(zhǎng)。只是 S1 和 C1 的回復(fù)。本消息由下列字段組成。
時(shí)間:4 字節(jié) 本字段必須包含對(duì)等段發(fā)送的時(shí)間(對(duì) C2 來(lái)說(shuō)是 S1,對(duì) S2 來(lái)說(shuō)是 C1)。
時(shí)間 2:4 字節(jié) 本字段必須包含先前發(fā)送的并被對(duì)端讀取的包的時(shí)間戳。
隨機(jī)回復(fù):1528 字節(jié) 本字段必須包含對(duì)端發(fā)送的隨機(jī)數(shù)據(jù)字段(對(duì) C2 來(lái)說(shuō)是 S1,對(duì) S2 來(lái)說(shuō)是 C1) 。 每個(gè)對(duì)等端可以用時(shí)間和時(shí)間 2 字段中的時(shí)間戳來(lái)快速地估計(jì)帶寬和延遲。 但這樣做可 能并不實(shí)用。

RTMP握手的這個(gè)過(guò)程就是完成了兩件事:1. 校驗(yàn)客戶端和服務(wù)器端RTMP協(xié)議版本號(hào),2. 是發(fā)了一堆數(shù)據(jù),猜想應(yīng)該是測(cè)試一下網(wǎng)絡(luò)狀況,看看有沒(méi)有傳錯(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ā)送用戶控制消息中的“流開(kāi)始”(Stream Begin)消息到客戶端。
  • 服務(wù)器發(fā)送命令消息中的“結(jié)果”(_result),通知客戶端連接的狀態(tài)。

注意:

  1. 這里面的connect 命令消息,命令里面包含什么東西,協(xié)議中沒(méi)有說(shuō),真實(shí)通信中要指定一些編解碼的信息,這些信息是以AMF格式發(fā)送的, 其中audioCodecs和videoCodecs這兩個(gè)指定音視頻編碼信息的不能少的。

  2. 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)收到了這么多消息。

  3. 服務(wù)器返回的_result命令類(lèi)型消息的payload length一般不會(huì)大于128字節(jié),但是在最新的nginx-rtmp中返回的消息長(zhǎng)度會(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庫(kù) 還會(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來(lái)進(jìn)行推流的,我這里采用的是一款純Java的推流庫(kù)yasea,之所以選擇這個(gè)推流第三方庫(kù),主要是為了了解上述的RTMP協(xié)議推流的流程。

接下來(lái)就結(jié)合代碼來(lái)分析下實(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)行匹配,接下來(lái)連接到視頻服務(wù)器,接下來(lái)通過(guò)handshake方法來(lái)進(jìn)行握手協(xié)議,接下來(lái)開(kāi)啟了兩個(gè)線程,這兩個(gè)線程是用來(lái)進(jìn)行讀寫(xiě)操作的,讀是讀取服務(wù)器返回的指令,寫(xiě)是向服務(wù)器發(fā)送指令,或者音視頻信息,最后開(kāi)啟一個(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命令,前面也說(shuō)到這個(gè)命令里面包含了很多東西
接下來(lái)就是服務(wù)器的返回信息分別是窗口大小與帶寬信息,這些信息則由前面說(shuō)的handleRxPacketLoop來(lái)讀取并進(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ù)器了,接下來(lái)服務(wù)器就會(huì)返回上面說(shuō)的結(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)建立起來(lái)了

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");
}

由于我們的視頻和音頻是分開(kāi)推流的,那么音視頻同步問(wèn)題怎么解決呢?

一般來(lái)說(shuō),視頻同步指的是視頻和音頻同步,也就是說(shuō)播放的聲音要和當(dāng)前顯示的畫(huà)面保持一致。想象以下,看一部電影的時(shí)候只看到人物嘴動(dòng)沒(méi)有聲音傳出;或者畫(huà)面是激烈的戰(zhàn)斗場(chǎng)景,而聲音不是槍炮聲卻是人物說(shuō)話的聲音,這是非常差的一種體驗(yàn)。
在視頻流和音頻流中已包含了其以怎樣的速度播放的相關(guān)數(shù)據(jù),視頻的幀率(Frame Rate)指示視頻一秒顯示的幀數(shù)(圖像數(shù));音頻的采樣率(Sample Rate)表示音頻一秒播放的樣本(Sample)的個(gè)數(shù)。可以使用以上數(shù)據(jù)通過(guò)簡(jiǎn)單的計(jì)算得到其在某一Frame(Sample)的播放時(shí)間,以這樣的速度音頻和視頻各自播放互不影響,在理想條件下,其應(yīng)該是同步的,不會(huì)出現(xiàn)偏差。但,理想條件是什么大家都懂得。如果用上面那種簡(jiǎn)單的計(jì)算方式,慢慢的就會(huì)出現(xiàn)音視頻不同步的情況。要不是視頻播放快了,要么是音頻播放快了,很難準(zhǔn)確的同步。這就需要一種隨著時(shí)間會(huì)線性增長(zhǎng)的量,視頻和音頻的播放速度都以該量為標(biāo)準(zhǔn),播放快了就減慢播放速度;播放快了就加快播放的速度。所以呢,視頻和音頻的同步實(shí)際上是一個(gè)動(dòng)態(tài)的過(guò)程,同步是暫時(shí)的,不同步則是常態(tài)。以選擇的播放速度量為標(biāo)準(zhǔn),快的等待慢的,慢的則加快速度,是一個(gè)你等我趕的過(guò)程。

播放速度標(biāo)準(zhǔn)量的的選擇一般來(lái)說(shuō)有以下三種:

  • 將視頻同步到音頻上,就是以音頻的播放速度為基準(zhǔn)來(lái)同步視頻。視頻比音頻播放慢了,加快其播放速度;快了,則延遲播放。
  • 將音頻同步到視頻上,就是以視頻的播放速度為基準(zhǔn)來(lái)同步音頻。
  • 將視頻和音頻同步外部的時(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è)推流庫(kù)要集成這些東西。。。

寫(xiě)在后面的話

推流已經(jīng)完成了,我們可以通過(guò)前面提到的vlc進(jìn)行查看,但是我們要做的是移動(dòng)端直播,所以下一篇就說(shuō)一下關(guān)于移動(dòng)端的播放與彈幕評(píng)論相關(guān)的知識(shí),peace~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,463評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,009評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,218評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,436評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,659評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,917評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,687評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容