当前位置: 首页 > 知识库问答 >
问题:

在android上通过UDP://显示mpegts流中的h264视频

龙俊德
2023-03-14

我已经试了几天让这个工作没有成功。我有的是一个设备,它产生一个h264视频流,它通过原始udp(而不是rtp)在mpegts容器中多播。我正试着在Android上的自定义应用程序中显示这个。

我读到Android内置的MediaPlayer同时支持h264(avc)和mpegts,但它不能处理UDP://流,所以我不能使用它(这是最简单的)。相反,我尝试手动将mpegts流解析为基本流,并将其传递给MediaCodec。MediaCodec通过SurfaceView的SurfaceView的SurfaceView的SurfaceView传递。无论我似乎尝试了什么,总会发生两件事(一旦我修复了异常等):

  • SurfaceView始终为黑色。
  • MediaCodec总是接受大约6-9个缓冲区,然后DequeueInputBuffer立即开始失败(返回-1),并且我无法对其他任何内容进行排队。

我可以将mpeg流拆分为TS包,然后将它们的有效载荷加入PES包。我已经尝试将完整的PES数据包(减去PES报头)传递到MediaCodec。

我还尝试通过在\x00\x00\x01上拆分并将它们单独传递到MediaCodec中,将PES数据包拆分为单独的NAL单元。

我还尝试在接收到SPS NAL单元之前不传递NAL单元,并首先使用buffer_flag_codec_config传递NAL单元。

所有这些都导致了上面提到的同样的事情。我不知道该尝试什么,所以任何帮助都将非常感谢。

有些点我还不太确定:

>

  • 我看到的几乎所有示例都从MediaExtractor获取MediaFormat,而我不能在流上使用它。少数不使用MediaExtractor的程序从未解释的字节字符串中设置csd-0和csd-1。我读到SPS数据包可以放在缓冲区中,所以我试了一下。

    我不确定数据需要如何传递到MediaCodec(这是它停止给我缓冲区的原因吗?)。我从这个so帖子中得到了传入单个NAL单元的想法:在Android中解码原始H264流?

    我用来做这个例子的其他引用:

    • MPEG-TS格式
    • PES格式
    • PES格式

    代码(对不起,很长):

    public class VideoPlayer extends Activity implements SurfaceHolder.Callback {
        private static final String TAG = VideoPlayer.class.getName();
    
        PlayerThread playerThread;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_video_player);
    
            SurfaceView view = (SurfaceView) findViewById(R.id.surface);
            view.getHolder().addCallback(this);
    
        }
    
        ...
    
        @Override
        public void surfaceCreated(SurfaceHolder surfaceHolder) {
            Log.d(TAG,"surfaceCreated");
        }
    
        @Override
        public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i2, int i3) {
            Log.d("main","surfaceChanged");
            if( playerThread == null ) {
                playerThread = new PlayerThread(surfaceHolder.getSurface());
                playerThread.start();
            }
        }
    
        ...
    

    PlayerThread是一个内部类,它从多播端口读取数据并将其传递给后台线程上的解析函数:

    class PlayerThread extends Thread {
        private final String TAG = PlayerThread.class.getName();
    
        MediaExtractor extractor;
        MediaCodec decoder;
        Surface surface;
        boolean running;
    
        ByteBuffer[] inputBuffers;
    
        public PlayerThread(Surface surface)
        {
            this.surface = surface;
    
            MediaFormat format = MediaFormat.createVideoFormat("video/avc",720,480);
    
            decoder = MediaCodec.createDecoderByType("video/avc");
            decoder.configure(format, surface, null, 0);
            decoder.start();
    
            inputBuffers = decoder.getInputBuffers();
    
        }
    
        ...
    
        @Override
        public void run() {
            running = true;
            try {
    
                String mcg = "239.255.0.1";
                MulticastSocket ms;
    
                ms = new MulticastSocket(1841);
                ms.joinGroup(new InetSocketAddress(mcg, 1841), NetworkInterface.getByName("eth0"));
                ms.setSoTimeout(4000);
                ms.setReuseAddress(true);
    
                byte[] buffer = new byte[65535];
                DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
    
                while (running) {
                    try {
                        ms.receive(dp);
                        parse(dp.getData());
    
                    } catch (SocketTimeoutException e) {
                        Log.d("thread", "timeout");
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    接收工作良好,每个数据报包包含两个TS包。它们被传递给解析函数:

        boolean first = true;
        ByteArrayOutputStream current =  new ByteArrayOutputStream();
        void parse(byte[] data) {
            ByteBuffer stream = ByteBuffer.wrap(data);
            // mpeg-ts stream header is 4 bytes starting with the sync byte
            if( stream.get(0) != 0x47 ) {
                Log.w(TAG, "got packet w/out mpegts header!");
                return;
            }
    
            ByteBuffer raw = stream.duplicate();
            // ts packets are 188 bytes
            raw.limit(188);
            TSPacket ts = new TSPacket(raw);
            if( ts.pid == 0x10 ) {
                processTS(ts);
            }
    
            // move to second packet
            stream.position(188);
            stream.limit(188*2);
            if( stream.get(stream.position()) != 0x47 ) {
                Log.w(TAG, "missing mpegts header!");
                return;
            }
            raw = stream.duplicate();
            raw.limit(188*2);
            ts = new TSPacket(raw);
            if( ts.pid == 0x10 ) {
                processTS(ts);
            }
        }
    

    TS数据包由TSPacket类解析:

    public class TSPacket {
        private final static String TAG = TSPacket.class.getName();
    
        class AdaptationField {
    
            boolean di;
            boolean rai;
            boolean espi;
            boolean hasPcr;
            boolean hasOpcr;
            boolean spf;
            boolean tpdf;
            boolean hasExtension;
    
            byte[] data;
    
            public AdaptationField(ByteBuffer raw) {
                // first byte is size of field minus size byte
                int count = raw.get() & 0xff;
    
                // second byte is flags
                BitSet flags = BitSet.valueOf(new byte[]{ raw.get()});
    
                di = flags.get(7);
                rai = flags.get(6);
                espi = flags.get(5);
                hasPcr = flags.get(4);
                hasOpcr = flags.get(3);
                spf = flags.get(2);
                tpdf = flags.get(1);
                hasExtension = flags.get(0);
    
                // the rest is 'data'
                if( count > 1 ) {
                    data = new byte[count-1];
                    raw.get(data);
                }
            }
        }
    
        boolean tei;
        boolean pus;
        boolean tp;
        int pid;
        boolean hasAdapt;
        boolean hasPayload;
        int counter;
        AdaptationField adaptationField;
        byte[] payload;
    
        public TSPacket(ByteBuffer raw) {
            // check for sync byte
            if( raw.get() != 0x47 ) {
                Log.e(TAG, "missing sync byte");
                throw new InvalidParameterException("missing sync byte");
            }
    
            // next 3 bits are flags
            byte b = raw.get();
            BitSet flags = BitSet.valueOf(new byte[] {b});
    
            tei = flags.get(7);
            pus = flags.get(6);
            tp = flags.get(5);
    
            // then 13 bits for pid
            pid = ((b << 8) | (raw.get() & 0xff) ) & 0x1fff;
    
            b = raw.get();
            flags = BitSet.valueOf(new byte[]{b});
    
            // then 4 more flags
            if( flags.get(7) || flags.get(6) ) {
                Log.e(TAG, "scrambled?!?!");
                // todo: bail on this packet?
            }
    
            hasAdapt = flags.get(5);
            hasPayload = flags.get(4);
    
            // counter
            counter = b & 0x0f;
    
            // optional adaptation field
            if( hasAdapt ) {
                adaptationField = new AdaptationField(raw);
            }
    
            // optional payload field
            if( hasPayload ) {
                payload = new byte[raw.remaining()];
                raw.get(payload);
            }
        }
    
    }
    

    然后传递给processTS函数:

        // a PES packet can span multiple TS packets, so we keep track of the 'current' one
        PESPacket currentPES;
        void processTS(TSPacket ts) {
            // payload unit start?
            if( ts.pus ) {
                if( currentPES != null ) {
                    Log.d(TAG,String.format("replacing pes: len=%d,size=%d", currentPES.length, currentPES.data.size()));
                }
                // start of new PES packet
                currentPES = new PESPacket(ts);
            } else if (currentPES != null ) {
                // continued PES
                currentPES.Add(ts);
            } else {
                // haven't got a start pes yet
                return;
            }
    
            if( currentPES.isFull() ) {
                long pts = currentPES.getPts();
                byte[] data = currentPES.data.toByteArray();
    
                int idx = 0;
    
                do {
                    int sidx = idx;
                    // find next NAL prefix
                    idx = Utility.indexOf(data, sidx+4, data.length-(sidx+4), new byte[]{0,0,1});
    
                    byte[] NAL;
                    if( idx >= 0 ) {
                        NAL = Arrays.copyOfRange(data, sidx, idx);
                    } else {
                        NAL = Arrays.copyOfRange(data, sidx, data.length);
                    }
    
                    // send SPS NAL before anything else
                    if( first ) {
                        byte type = NAL[3] == 0 ? NAL[4] : NAL[3];
                        if( (type & 0x1f) == 7 ) {
                            Log.d(TAG, "found sps!");
    
                            int ibs = decoder.dequeueInputBuffer(1000);
                            if (ibs >= 0) {
                                ByteBuffer sinput = inputBuffers[ibs];
                                sinput.clear();
                                sinput.put(NAL);
    
                                decoder.queueInputBuffer(ibs, 0, NAL.length, 0, MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
                                Log.d(TAG, "sent sps");
                                first = false;
                            } else
                                Log.d(TAG, String.format("could not send sps! %d", ibs));
                        }
                    } else {
    
                        // put in decoder?
                        int ibs = decoder.dequeueInputBuffer(1000);
                        if (ibs >= 0) {
                            ByteBuffer sinput = inputBuffers[ibs];
                            sinput.clear();
                            sinput.put(NAL);
    
                            decoder.queueInputBuffer(ibs, 0, NAL.length, 0, 0);
                            Log.d(TAG, "buffa");
                        } 
                    }
                } while( idx >= 0 );
    
                // finished with this pes
                currentPES = null;
            }
        }
    

    PES数据包由PESPacket类解析:

    public class PESPacket {
        private final static String TAG = PESPacket.class.getName();
    
        int id;
        int length;
    
        boolean priority;
        boolean dai;
        boolean copyright;
        boolean origOrCopy;
        boolean hasPts;
        boolean hasDts;
        boolean hasEscr;
        boolean hasEsRate;
        boolean dsmtmf;
        boolean acif;
        boolean hasCrc;
        boolean pesef;
        int headerDataLength;
    
        byte[] headerData;
        ByteArrayOutputStream data = new ByteArrayOutputStream();
    
        public PESPacket(TSPacket ts) {
            if( ts == null || !ts.pus) {
                Log.e(TAG, "invalid ts passed in");
                throw new InvalidParameterException("invalid ts passed in");
            }
    
            ByteBuffer pes = ByteBuffer.wrap(ts.payload);
    
            // start code
            if( pes.get() != 0 || pes.get() != 0 || pes.get() != 1 ) {
                Log.e(TAG, "invalid start code");
                throw new InvalidParameterException("invalid start code");
            }
    
            // stream id
            id = pes.get() & 0xff;
    
            // packet length
            length = pes.getShort() & 0xffff;
    
            // this is supposedly allowed for video
            if( length == 0 ) {
                Log.w(TAG, "got zero-length PES?");
            }
    
            if( id != 0xe0 ) {
                Log.e(TAG, String.format("unexpected stream id: 0x%x", id));
                // todo: ?
            }
    
            // for 0xe0 there is an extension header starting with 2 bits '10'
            byte b = pes.get();
            if( (b & 0x30) != 0 ) {
                Log.w(TAG, "scrambled ?!?!");
                // todo: ?
            }
    
            BitSet flags = BitSet.valueOf(new byte[]{b});
            priority = flags.get(3);
            dai = flags.get(2);
            copyright = flags.get(1);
            origOrCopy = flags.get(0);
    
            flags = BitSet.valueOf(new byte[]{pes.get()});
            hasPts = flags.get(7);
            hasDts = flags.get(6);
            hasEscr = flags.get(5);
            hasEsRate = flags.get(4);
            dsmtmf = flags.get(3);
            acif = flags.get(2);
            hasCrc = flags.get(1);
            pesef = flags.get(0);
    
            headerDataLength = pes.get() & 0xff;
    
            if( headerDataLength > 0 ) {
                headerData = new byte[headerDataLength];
                pes.get(headerData);
            }
    
            WritableByteChannel channel = Channels.newChannel(data);
            try {
                channel.write(pes);
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            // length includes optional pes header,
            length = length - (3 + headerDataLength);
        }
    
        public void Add(TSPacket ts) {
            if( ts.pus ) {
                Log.e(TAG, "don't add start of PES packet to another packet");
                throw new InvalidParameterException("ts packet marked as new pes");
            }
    
            int size = data.size();
            int len = length - size;
            len = ts.payload.length > len ? len : ts.payload.length;
            data.write(ts.payload, 0, len);
        }
    
        public boolean isFull() {
            return (data.size() >= length );
        }
    
        public long getPts() {
            if( !hasPts || headerDataLength < 5 )
                return 0;
    
            ByteBuffer hd = ByteBuffer.wrap(headerData);
            long pts = ( ((hd.get() & 0x0e) << 29)
                        | ((hd.get() & 0xff) << 22)
                        | ((hd.get() & 0xfe) << 14)
                        | ((hd.get() & 0xff) << 7)
                        | ((hd.get() & 0xfe) >>> 1));
    
            return pts;
        }
    }
    
  • 共有1个答案

    尹昀
    2023-03-14

    所以我最终发现,即使我使用的是输出表面,我也必须手动排出输出缓冲区。通过调用Decoder.DequeueOutputBuffer,然后调用Decoder.ReleaseOutputBuffer,输入缓冲区按预期工作。

    我还能够通过传入单独的NAL单元和完全访问单元(每个PES数据包一个)来获得输出,但是通过传入完全访问单元,我获得了最清晰的视频。

     类似资料:
    • 如上所述,我使用FFmpeg(命令行)通过UDP传输网络摄像头。在客户端,我使用Java OpenCV,即捕获行<代码>视频捕获。打开(“udp://xx.xx.xx.xx:xx) 。如果我将流作为mpegts(ffmpeg-f mpegts)发送,我可以显示流,但是;如果我把它作为rawvideo(ffmpeg-f rawvideo)发送,我不能。 是否有要设置的参数(如CvType)?

    • 我正在为android编写一个rtp视频流,它从android本地套接字读取h264编码的数据并将其打包。问题是我做到了,但我在客户端(Voip)中不断收到黑帧。 通信方式如下:Android- 有几件事我还不明白: 1) Android的mediarecorder给了我一个原始的h264流,我怎么知道NAL何时根据该流开始/结束?它没有任何0x000001模式,但它有一个0x0000(我假设它是

    • 我刚刚接触Android,最近我在我的应用程序中添加了一个CardView来显示一些文本框和图像,效果很好。 然而,当我试图显示视频时,卡仍然是空的。有人能帮我理解我做错了什么吗? 我这样定义了图像视图: 最初,我尝试将视频设置为相关活动的onCreate,如下所示: 那不起作用,不知道为什么。所以从相关的中,我使用从URL获取视频。由于UI更改,在onProgressMethod中使用相同的代码

    • 应用程序创建的h264帧被发送到标准输出,在标准输出中,使用ffmpeg将该流重新复用为mp4,并将其传递给服务器,服务器根据请求将其传递给客户端。 这是个好办法吗?这甚至可能创建一个低延迟30fps视频流使用这种方法?

    • 我正在手动读取一个RTP/H264流,并将H264帧传递给Android MediaCodec。我使用“markerbit”作为帧的边框。MediaCodec被绑定到一个OpenGL纹理(SurfaceTexture)。总的来说,一切都很好。但解码器似乎在缓冲帧。如果我把一个帧放入解码器,它不会立即呈现到纹理上。当我在解码器中多放2-3帧后,第一帧被渲染到纹理中。 我正在针对Android4.4.

    • 我有一个项目,我被要求在android中显示一个视频流,该流是原始的H.264,我正在连接到一个服务器,并将从服务器接收一个字节流。 基本上,我想知道有没有一种方法可以将原始字节发送到android的解码器并显示在Surface上? 我使用Android4.1中新的MediaCodec和MediaExtractor API成功地解码了包装在mp4容器中的H264,不幸的是,我没有找到使用这些API