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

寫在前面的話

前面一篇文章已經(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)。

注意:

  1. 這里面的connect 命令消息巷挥,命令里面包含什么東西桩卵,協(xié)議中沒有說,真實(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命令類型消息的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~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末路幸,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子付翁,更是在濱河造成了極大的恐慌简肴,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件百侧,死亡現(xiàn)場離奇詭異砰识,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)佣渴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門辫狼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人辛润,你說我怎么就攤上這事膨处。” “怎么了砂竖?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵真椿,是天一觀的道長。 經(jīng)常有香客問我乎澄,道長突硝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任置济,我火速辦了婚禮解恰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘舟肉。我一直安慰自己修噪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布路媚。 她就那樣靜靜地躺著,像睡著了一般樊销。 火紅的嫁衣襯著肌膚如雪整慎。 梳的紋絲不亂的頭發(fā)上脏款,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天,我揣著相機(jī)與錄音裤园,去河邊找鬼撤师。 笑死,一個(gè)胖子當(dāng)著我的面吹牛拧揽,可吹牛的內(nèi)容都是我干的剃盾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼淤袜,長吁一口氣:“原來是場噩夢啊……” “哼痒谴!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起铡羡,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤积蔚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后烦周,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尽爆,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年读慎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了漱贱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡夭委,死狀恐怖饱亿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情闰靴,我是刑警寧澤彪笼,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站蚂且,受9級(jí)特大地震影響配猫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杏死,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一泵肄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧淑翼,春花似錦腐巢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至遭京,卻和暖如春胃惜,著一層夾襖步出監(jiān)牢的瞬間泞莉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國打工船殉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鲫趁,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓利虫,卻偏偏與公主長得像挨厚,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子糠惫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

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