Android RTMP推流之MediaCodec硬編碼一(H.264進(jìn)行flv封裝)

在前面Android平臺(tái)下使用FFmpeg進(jìn)行RTMP推流(攝像頭推流)的文章中,介紹了如何使用FFmpeg進(jìn)行H264編碼和Rtmp推流。接下來講分幾篇文章來介紹如何使用Android系統(tǒng)的MediaCodec進(jìn)行H264硬編碼,然后封裝推流。這一塊涉及的內(nèi)容很多,其中涉及一些基礎(chǔ)知識(shí)也會(huì)有單獨(dú)文章介紹比如flv格式。這篇文章主要介紹如何用MediaCodec進(jìn)行編碼,然后將編碼后的數(shù)據(jù)進(jìn)行flv封裝。

文章同步項(xiàng)目源碼地址
注意版本為V1.3

3.png

MediaCodec介紹

學(xué)習(xí)個(gè)模塊內(nèi)容當(dāng)然是參考官方文檔Android MediaCodec。但有些兄弟可能沒有大多耐心看英文,那我在推薦一個(gè)中文版的MediaCodec官方文檔譯文,如果還是沒耐心看,或者沒看懂,那就試試看下我的理解。
先上一張圖:

1.png

這個(gè)圖也是官網(wǎng)上摳下來的。對(duì)這個(gè)圖的理解很關(guān)鍵。我先總結(jié)一下:

  • MediaCodec編碼器包含兩個(gè)緩沖區(qū),一個(gè)輸入緩沖區(qū),一個(gè)輸出緩沖區(qū)。
  • 客戶端先從MediaCodec獲取一個(gè)可用的輸入緩沖區(qū),然后將待編碼的數(shù)據(jù)填充到緩沖區(qū),然后交給MediaCodec去處理。
  • 客戶端從輸出緩沖區(qū)獲取已經(jīng)處理好的數(shù)據(jù),客戶端得到數(shù)據(jù)后并處理后,釋放空間,最后將緩沖區(qū)還給MediaCodec。

我把整條線簡(jiǎn)單的描述了一下。也就是整個(gè)編碼流程,客戶端是如何操作的。下面我們要深入了解MediaoCodec如何工作,還是先上圖

2.png

這里我也總結(jié)下:

  • 要用MediaCodec,首先需要?jiǎng)?chuàng)建,根據(jù)我們想要的編碼格式創(chuàng)建。創(chuàng)建后就是Uninitialized狀態(tài)
  • 創(chuàng)建完成之后還不能直接用,我們需要進(jìn)行配置,進(jìn)入Configured。這時(shí)候就準(zhǔn)備就緒了
  • Configured后,就可以start。進(jìn)行運(yùn)行階段了。
  • 運(yùn)行階段又分3個(gè)子狀態(tài)。start()后就進(jìn)入Flushed狀態(tài)。
  • 當(dāng)客戶端獲取一個(gè)有效的輸入緩沖區(qū)后,就進(jìn)入了Running,而MediaCodec大部分時(shí)間在這個(gè)狀態(tài)
  • 如果客戶端將得到的輸入緩沖區(qū)入隊(duì)時(shí)帶有末尾標(biāo)記時(shí),編碼器就進(jìn)入End of Stream狀態(tài),這時(shí)候就不再接受后面緩沖區(qū)的輸入
  • stop之后就會(huì)重新進(jìn)入U(xiǎn)ninitialized狀態(tài)。
  • 如果出現(xiàn)錯(cuò)誤就會(huì)進(jìn)入Error狀態(tài)

到這里我們就簡(jiǎn)單的吧MediaCodec介紹完了。當(dāng)然我只是簡(jiǎn)單的介紹,大概了解后,我們先用起來,然后自己再體會(huì)就知道了。


MediaCodec編碼

創(chuàng)建并配置MediaCodec

我們按前面的流程使用MediaCodec。先創(chuàng)建MediaCodec
先上代碼


    private void initMediaCodec() {
        int bitrate = 2 * WIDTH * HEIGHT * FRAME_RATE / 20;
        try {
            MediaCodecInfo mediaCodecInfo = selectCodec(VCODEC_MIME);
            if (mediaCodecInfo == null) {
                Toast.makeText(this, "mMediaCodec null", Toast.LENGTH_LONG).show();
                throw new RuntimeException("mediaCodecInfo is Empty");
            }
            LogUtils.w("MediaCodecInfo " + mediaCodecInfo.getName());
            mMediaCodec = MediaCodec.createByCodecName(mediaCodecInfo.getName());
            MediaFormat mediaFormat = MediaFormat.createVideoFormat(VCODEC_MIME, WIDTH, HEIGHT);
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
            mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                    MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
            mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
            mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            mMediaCodec.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  • 查找編碼器
    MediaCodecInfo mediaCodecInfo = selectCodec(VCODEC_MIME);
    我們看到selectCodec方法。我們要使用H.264編碼,所以傳入的參數(shù)
    private static final String VCODEC_MIME = "video/avc";
    private MediaCodecInfo selectCodec(String mimeType) {
        int numCodecs = MediaCodecList.getCodecCount();
        for (int i = 0; i < numCodecs; i++) {
            MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
            //是否是編碼器
            if (!codecInfo.isEncoder()) {
                continue;
            }
            String[] types = codecInfo.getSupportedTypes();
            LogUtils.w(Arrays.toString(types));
            for (String type : types) {
                LogUtils.e("equal " + mimeType.equalsIgnoreCase(type));
                if (mimeType.equalsIgnoreCase(type)) {
                    LogUtils.e("codecInfo " + codecInfo.getName());
                    return codecInfo;
                }
            }
        }
        return null;
    }

這段邏輯主要是獲取系統(tǒng)的編碼器并查找是否有我們需要的編碼器并返回其信息。得到信息后我們就可以創(chuàng)建MediaCodec

mMediaCodec = MediaCodec.createByCodecName(mediaCodecInfo.getName());
  • 配置編碼器信息
    前面我們已經(jīng)查找并創(chuàng)建了編碼器,這一步就是進(jìn)行參數(shù)配置。主要是setInteger等方法進(jìn)行類似key-value的設(shè)置。如:碼率KEY_BIT_RATE、編碼像素格式KEY_COLOR_FORMAT、幀率KEY_FRAME_RATE。
    這里要注意KEY_COLOR_FORMAT像素格式的設(shè)置,后面涉及到格式的轉(zhuǎn)換,同時(shí)不同的設(shè)備可能支持的格式不同,我測(cè)試的設(shè)備就不支持COLOR_FormatYUV420SemiPlanar。
  • 配置
    mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    這一步也是必須的

編碼

前面的文章我們已經(jīng)講到了如何采集獲取Camera的數(shù)據(jù),這里就不再累述。直接看到Camera.PreviewCallbackonPreviewFrame(final byte[] data, Camera camera)回調(diào)方法。

    public class StreamIt implements Camera.PreviewCallback {
        @Override
        public void onPreviewFrame(final byte[] data, Camera camera) {
            long endTime = System.currentTimeMillis();
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    encodeTime = System.currentTimeMillis();
                    flvPackage(data);
                    LogUtils.w("編碼第:" + (encodeCount++) + "幀,耗時(shí):" + (System.currentTimeMillis() - encodeTime));
                }
            });
            LogUtils.d("采集第:" + (++count) + "幀,距上一幀間隔時(shí)間:"
                    + (endTime - previewTime) + "  " + Thread.currentThread().getName());
            previewTime = endTime;
        }
    }

這個(gè)回調(diào)方法大家就很首席了data就是采集到的原始YUV數(shù)據(jù)。為了方便調(diào)試,我就把第幾幀和編碼時(shí)間以及采集時(shí)間打印出來。而這里的YUV數(shù)據(jù)和Camera的參數(shù)設(shè)置有關(guān)params.setPreviewFormat(ImageFormat.YV12);系統(tǒng)默認(rèn)使用的N21,這里我使用YV12格式。
接下來重點(diǎn)就是flvPackage(data);調(diào)用了

    private void flvPackage(byte[] buf) {
        final int LENGTH = HEIGHT * WIDTH;
        //YV12數(shù)據(jù)轉(zhuǎn)化成COLOR_FormatYUV420Planar
        LogUtils.d(LENGTH + "  " + (buf.length - LENGTH));
        for (int i = LENGTH; i < (LENGTH + LENGTH / 4); i++) {
            byte temp = buf[i];
            buf[i] = buf[i + LENGTH / 4];
            buf[i + LENGTH / 4] = temp;
//            char x = 128;
//            buf[i] = (byte) x;
        }
        ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
        ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
        try {
            //查找可用的的input buffer用來填充有效數(shù)據(jù)
            int bufferIndex = mMediaCodec.dequeueInputBuffer(-1);
            if (bufferIndex >= 0) {
                //數(shù)據(jù)放入到inputBuffer中
                ByteBuffer inputBuffer = inputBuffers[bufferIndex];
                inputBuffer.clear();
                inputBuffer.put(buf, 0, buf.length);
                //把數(shù)據(jù)傳給編碼器并進(jìn)行編碼
                mMediaCodec.queueInputBuffer(bufferIndex, 0,
                        inputBuffers[bufferIndex].position(),
                        System.nanoTime() / 1000, 0);
                MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

                //輸出buffer出隊(duì),返回成功的buffer索引。
                int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
                while (outputBufferIndex >= 0) {
                    ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                    //進(jìn)行flv封裝
                    mFlvPacker.onVideoData(outputBuffer, bufferInfo);
                    mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                    outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
                }
            } else {
                LogUtils.w("No buffer available !");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

我們看到第一步有一個(gè)格式轉(zhuǎn)換,YV12數(shù)據(jù)轉(zhuǎn)化成COLOR_FormatYUV420Planar。因?yàn)榫幋a器支持的輸入是COLOR_FormatYUV420Planar,而我們采集到的是YV12。所以需要轉(zhuǎn)換。兩者的區(qū)別就是U、V分量顛倒了個(gè)位置。在Android平臺(tái)下使用FFmpeg進(jìn)行RTMP推流(攝像頭推流)有具體介紹。

接下來就是關(guān)鍵部分了MediaCodec進(jìn)行H264編碼??蛻舳说氖褂昧鞒涛覀儼凑諏?duì)圖1的總結(jié)來進(jìn)行操作
首先獲取編碼器的輸入和輸出緩沖區(qū)

        ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
        ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();

接下來獲取一個(gè)可用的輸入緩沖區(qū)索引

int bufferIndex = mMediaCodec.dequeueInputBuffer(-1);

如果返回>0說明有效。然后獲取到對(duì)應(yīng)的ByteBuffer
ByteBuffer inputBuffer = inputBuffers[bufferIndex];
接下來就是講圖像數(shù)據(jù)填充到inputBuffer中。

                inputBuffer.clear();
                inputBuffer.put(buf, 0, buf.length);

然后告訴編碼器開始編碼

 //把數(shù)據(jù)傳給編碼器并進(jìn)行編碼
                mMediaCodec.queueInputBuffer(bufferIndex, 0,
                inputBuffers[bufferIndex].position(),
                System.nanoTime() / 1000, 0);

接下來就是獲取編碼后的數(shù)據(jù),這里先得到輸出緩沖區(qū)索引

int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);

然后就可以得到對(duì)應(yīng)的ByteBuffer。也就是編碼后的數(shù)據(jù),得到數(shù)據(jù)后我們就可以進(jìn)行flv封裝。
最后我們需要釋放緩沖區(qū)

mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);

到這里我們就了解了如何具體使用MediaCodec,接下來就是如何進(jìn)行flv封裝


flv封裝

前面已經(jīng)講到如何進(jìn)行H264編碼,并得到編碼后的數(shù)據(jù)。接下來就是如何將原始的H264數(shù)據(jù)封裝成flv格式的數(shù)據(jù)。在將flv封裝之前,大家一定要熟悉flv的格式。flv格式相對(duì)比較簡(jiǎn)單,可以參考flv格式詳解+實(shí)例剖析。否則接下來的內(nèi)容大家會(huì)一臉懵逼。在講代碼前,還是先總結(jié)下流程:

  • flv文件封裝視頻數(shù)據(jù)前先寫入flv頭,metadata數(shù)據(jù)。
  • MediaCodec進(jìn)行編碼后的第一個(gè)數(shù)據(jù)是sps、pps數(shù)據(jù),也是flv中的第一個(gè)video tag。
  • 后面收到的MediaCodec編碼后的數(shù)據(jù)就是正常的視頻h264數(shù)據(jù),封裝到flv中。

封裝h264的調(diào)用:

//進(jìn)行flv封裝
mFlvPacker.onVideoData(outputBuffer, bufferInfo);

我們進(jìn)入代碼看到:

    @Override
    public void onVideoData(ByteBuffer bb, MediaCodec.BufferInfo bi) {
        mAnnexbHelper.analyseVideoData(bb, bi);
    }

再跟進(jìn)方法

    /**
     * 將硬編得到的視頻數(shù)據(jù)進(jìn)行處理生成每一幀視頻數(shù)據(jù),然后傳給flv打包器
     * @param bb 硬編后的數(shù)據(jù)buffer
     * @param bi 硬編的BufferInfo
     */
    public void analyseVideoData(ByteBuffer bb, MediaCodec.BufferInfo bi) {
        bb.position(bi.offset);
        bb.limit(bi.offset + bi.size);

        ArrayList<byte[]> frames = new ArrayList<>();
        boolean isKeyFrame = false;

        while(bb.position() < bi.offset + bi.size) {
            byte[] frame = annexbDemux(bb, bi);
            if(frame == null) {
                LogUtils.e("annexb not match.");
                break;
            }
            // ignore the nalu type aud(9)
            if (isAccessUnitDelimiter(frame)) {
                continue;
            }
            // for pps
            if(isPps(frame)) {
                mPps = frame;
                continue;
            }
            // for sps
            if(isSps(frame)) {
                mSps = frame;
                continue;
            }
            // for IDR frame
            if(isKeyFrame(frame)) {
                isKeyFrame = true;
            } else {
                isKeyFrame = false;
            }
            byte[] naluHeader = buildNaluHeader(frame.length);
            frames.add(naluHeader);
            frames.add(frame);
        }
        if (mPps != null && mSps != null && mListener != null && mUploadPpsSps) {
            if(mListener != null) {
                mListener.onSpsPps(mSps, mPps);
            }
            mUploadPpsSps = false;
        }
        if(frames.size() == 0 || mListener == null) {
            return;
        }
        int size = 0;
        for (int i = 0; i < frames.size(); i++) {
            byte[] frame = frames.get(i);
            size += frame.length;
        }
        byte[] data = new byte[size];
        int currentSize = 0;
        for (int i = 0; i < frames.size(); i++) {
            byte[] frame = frames.get(i);
            System.arraycopy(frame, 0, data, currentSize, frame.length);
            currentSize += frame.length;
        }
        if(mListener != null) {
            mListener.onVideo(data, isKeyFrame);
        }
    }

這個(gè)方法主要是從編碼后的數(shù)據(jù)中解析得到NALU,然后判斷NALU的類型,最后再把數(shù)據(jù)回調(diào)給FlvPacker去處理。那如何解析得到NALU,我們看到annexbDemux(bb, bi)方法

    /**
     * 從硬編出來的數(shù)據(jù)取出一幀nal
     * @param bb
     * @param bi
     * @return
     */
    private byte[] annexbDemux(ByteBuffer bb, MediaCodec.BufferInfo bi) {
        AnnexbSearch annexbSearch = new AnnexbSearch();
        avcStartWithAnnexb(annexbSearch, bb, bi);

        if (!annexbSearch.match || annexbSearch.startCode < 3) {
            return null;
        }

        for (int i = 0; i < annexbSearch.startCode; i++) {
            bb.get();
        }

        ByteBuffer frameBuffer = bb.slice();
        int pos = bb.position();
        while (bb.position() < bi.offset + bi.size) {
            avcStartWithAnnexb(annexbSearch, bb, bi);
            if (annexbSearch.match) {
                break;
            }
            bb.get();
        }

        int size = bb.position() - pos;
        byte[] frameBytes = new byte[size];
        frameBuffer.get(frameBytes);
        return frameBytes;
    }

方法返回得到NALU數(shù)據(jù),那如何解析的呢,看到avcStartWithAnnexb(annexbSearch, bb, bi);方法調(diào)用

   /**
     * 從硬編出來的byteBuffer中查找nal
     * @param as
     * @param bb
     * @param bi
     */
    private void avcStartWithAnnexb(AnnexbSearch as, ByteBuffer bb, MediaCodec.BufferInfo bi) {
        as.match = false;
        as.startCode = 0;
        int pos = bb.position();
        while (pos < bi.offset + bi.size - 3) {
            // not match.
            if (bb.get(pos) != 0x00 || bb.get(pos + 1) != 0x00) {
                break;
            }

            // match N[00] 00 00 01, where N>=0
            if (bb.get(pos + 2) == 0x01) {
                as.match = true;
                as.startCode = pos + 3 - bb.position();
                break;
            }
            pos++;
        }
    }

這里邏輯也比較簡(jiǎn)單,遍歷尋找NALU的開頭,開頭是有0x0000010x00000001開頭。這里找到匹配的位置后設(shè)置的到AnnexbSearch中。

我們回到analyseVideoData方法,在調(diào)用byte[] frame = annexbDemux(bb, bi);后我們已經(jīng)得到NALU數(shù)據(jù),接下來就是判斷NALU類型

        while(bb.position() < bi.offset + bi.size) {
            byte[] frame = annexbDemux(bb, bi);
            if(frame == null) {
                LogUtils.e("annexb not match.");
                break;
            }
            // ignore the nalu type aud(9)
            if (isAccessUnitDelimiter(frame)) {
                continue;
            }
            // for pps
            if(isPps(frame)) {
                mPps = frame;
                continue;
            }
            // for sps
            if(isSps(frame)) {
                mSps = frame;
                continue;
            }
            // for IDR frame
            if(isKeyFrame(frame)) {
                isKeyFrame = true;
            } else {
                isKeyFrame = false;
            }
            byte[] naluHeader = buildNaluHeader(frame.length);
            frames.add(naluHeader);
            frames.add(frame);
        }
        if (mPps != null && mSps != null && mListener != null && mUploadPpsSps) {
            if(mListener != null) {
                mListener.onSpsPps(mSps, mPps);
            }
            mUploadPpsSps = false;
        }

首先需要知道NALU是有header+Payload組成。而header固定1個(gè)字節(jié),由3個(gè)部分組成forbidden_bit(1bit),nal_reference_bit(2bits)(優(yōu)先級(jí)),nal_unit_type(5bits)(類型)。
這里重點(diǎn)看到類型:

3.jpg

所以我們看到代碼的判斷,解析第一個(gè)字節(jié)就可以啦:

    private boolean isSps(byte[] frame) {
        if (frame.length < 1) {
            return false;
        }
        // 5bits, 7.3.1 NAL unit syntax,
        // H.264-AVC-ISO_IEC_14496-10.pdf, page 44.
        //  7: SPS, 8: PPS, 5: I Frame, 1: P Frame
        int nal_unit_type = (frame[0] & 0x1f);
        return nal_unit_type == SPS;
    }

    private boolean isPps(byte[] frame) {
        if (frame.length < 1) {
            return false;
        }
        // 5bits, 7.3.1 NAL unit syntax,
        // H.264-AVC-ISO_IEC_14496-10.pdf, page 44.
        //  7: SPS, 8: PPS, 5: I Frame, 1: P Frame
        int nal_unit_type = (frame[0] & 0x1f);
        return nal_unit_type == PPS;
    }

    private boolean isKeyFrame(byte[] frame) {
        if (frame.length < 1) {
            return false;
        }
        // 5bits, 7.3.1 NAL unit syntax,
        // H.264-AVC-ISO_IEC_14496-10.pdf, page 44.
        //  7: SPS, 8: PPS, 5: I Frame, 1: P Frame
        int nal_unit_type = (frame[0] & 0x1f);
        return nal_unit_type == IDR;
    }

回到analyseVideoData方法,當(dāng)sps和pps都回去到后,就可以調(diào)用mListener.onSpsPps(mSps, mPps);把數(shù)據(jù)回調(diào)給FlvPacker。我們看到實(shí)現(xiàn)部分

    @Override
    public void onSpsPps(byte[] sps, byte[] pps) {
        if(packetListener == null) {
            return;
        }
        //寫入Flv header信息
        writeFlvHeader();
        //寫入Meta 相關(guān)信息
        writeMetaData();
        //寫入第一個(gè)視頻信息
        writeFirstVideoTag(sps, pps);
        //寫入第一個(gè)音頻信息
        writeFirstAudioTag();
        mStartTime = System.currentTimeMillis();
        isHeaderWrite = true;
    }

因?yàn)閟ps和pps有且僅有一個(gè)而且是第一個(gè)。所以在這里,同事寫入flv的頭部信息和metaData數(shù)據(jù),然后將sps和pps信息寫入。至于每個(gè)tag的封裝,這里就不做講解了,大家針對(duì)前面flv格式詳解+實(shí)例剖析文章,再對(duì)照代碼就很清晰了。
再看到mListener.onVideo(data, isKeyFrame);

    @Override
    public void onVideo(byte[] video, boolean isKeyFrame) {
        if(packetListener == null || !isHeaderWrite) {
            return;
        }
        int compositionTime = (int) (System.currentTimeMillis() - mStartTime);
        int packetType = INTER_FRAME;
        if(isKeyFrame) {
            isKeyFrameWrite = true;
            packetType = KEY_FRAME;
        }
        //確保第一幀是關(guān)鍵幀,避免一開始出現(xiàn)灰色模糊界面
        if(!isKeyFrameWrite) {
            return;
        }

        int videoPacketSize = VIDEO_HEADER_SIZE + video.length;
        int dataSize = videoPacketSize + FLV_TAG_HEADER_SIZE;
        int size = dataSize + PRE_SIZE;
        ByteBuffer buffer = ByteBuffer.allocate(size);
        FlvPackerHelper.writeFlvTagHeader(buffer, FlvPackerHelper.FlvTag.Video, videoPacketSize, compositionTime);
        FlvPackerHelper.writeH264Packet(buffer, video, isKeyFrame);
        buffer.putInt(dataSize);
        packetListener.onPacket(buffer.array(), packetType);
    }

這里就是正常的視頻NALU數(shù)據(jù)寫入了。
兩個(gè)回調(diào)方法最后都調(diào)用了packetListener.onPacket(buffer.array(), packetType);。這個(gè)packetListener就是我們CameraMediaCodecActivity中設(shè)置的回調(diào)

        mFlvPacker.setPacketListener(new Packer.OnPacketListener() {
            @Override
            public void onPacket(byte[] data, int packetType) {
                IOUtils.write(mOutStream, data, 0, data.length);
                LogUtils.w(data.length + " " + packetType);
            }
        });

代碼會(huì)簡(jiǎn)單就是講封裝好的flv數(shù)據(jù)寫入到文件中。

到此,我們就基本了解如何使用MediaCodec進(jìn)行H.264硬編碼,然后坐Flv格式封裝。后續(xù)會(huì)陸續(xù)推出將封裝的flv數(shù)據(jù)進(jìn)行RTMP推流,請(qǐng)大家關(guān)注!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容