在安卓上通过udp://显示来自mpegts流的h264视频

6

在Android上从UDP传输的MPEGTS流中显示h264视频。

我尝试了几天,但没有成功。我的设备会生成一个h264视频流,并通过mpegts容器以裸udp(不是rtp)的形式进行多播。我正在尝试在Android上的自定义应用程序中显示它。

据我所知,Android内置的MediaPlayer支持h264(avc)和mpegts,但不支持udp://流,因此我无法使用它(这将是最简单的方法)。相反,我已经尝试手动解析mpegts流为基本流,并将其传递给已传递SurfaceView表面的MediaCodec。无论我尝试什么,始终会发生两件事(一旦我修复了异常等):

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

我可以将mpeg流拆分为TS数据包,然后将其有效负载合并为PES数据包。我尝试向MediaCodec传递完整的PES数据包(不包括PES头)。

我还尝试通过在\x00\x00\x01处拆分来将PES数据包拆分为单个NAL单元,并将它们逐个传递到MediaCodec。

我还尝试在收到SPS NAL单元之前暂停传递NAL单元,并首先通过BUFFER_FLAG_CODEC_CONFIG传递它。

所有这些都导致上述相同的问题。我已经没有想法要尝试什么,所以任何帮助都将不胜感激。

我仍然不确定的一些要点:

  • 几乎所有我看到的示例都从MediaExtractor获取MediaFormat,但我无法在流上使用它。 其中只有少数使用MediaExtractor显式设置csd-0和csd-1,但未解释其中的bytestrings。 我读到SPS数据包可以放入缓冲区中,因此我尝试了这个。

  • 我不确定要传递给presentationTimeUs什么。TS数据包具有PCR,而PES数据包具有PTS,但我不知道API需要什么以及这些之间的关系。

  • 我不确定如何将数据传递到MediaCodec中(这是为什么它停止给我提供缓冲区吗?)。我从这个帖子中得到了单独传递NAL单元的想法: Decoding Raw H264 stream in android?

我用于制作此示例的其他参考资料:

代码(抱歉代码比较长):

我刚刚在AndroidStudio的基本模板中创建了一个测试应用程序,其中大部分是样板文件,所以我只会粘贴与视频相关的内容。

SurfaceView在xml中定义,所以当它创建/更改时,请获取它并获取表面。

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

TSPacket类解析TS数据包:

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

了解MediaCodec解码器需求的最好方法是查看MediaCodec编码器发出的内容。实质上,每个缓冲区对应一个NAL单元,并带有一个包含SPS和PPS数据的编解码器特定数据块。通过记录十六进制转储,您可以看到编解码器需要的精确格式。直到接收到SPS / PPS,解码器才会启动。据我所知,这些信息中没有任何文档记录。 - fadden
fadden - 需要在同一个缓冲区中同时包含SPS和PPS吗?它们是单独的NAL单元,因此PPS只是在SPS之后传递(但没有CODEC_CONFIG标志)。 - bj0
我认为它需要在同一个缓冲区中,但如果您正在设置MediaFormat对象上的“csd-0”和“csd-1”缓冲区,则可以将其放入其中一个。http://bigflake.com/mediacodec/#EncodeDecodeTest 在两种情况下都使用单个组合缓冲区来练习MediaFormat方法和CODEC_CONFIG方法。https://dev59.com/kmIj5IYBdhLWcg3w8JRZ 显示了csd-0 / csd-1。 - fadden
我没有设置csd,因为我不确定应该放什么。我已经阅读了一些示例,但是找不到与我的情况相匹配的(它们都有一个源,从中获取mediaformat或mediainfo)。我刚刚尝试将SPS和PPS NAL放在同一个缓冲区中,但没有任何变化。我想知道,是否因为输出没有出去而导致我用完了输入缓冲区?除了放置表面之外,我需要做更多的事情吗? - bj0
如果你的输入缓冲区不足,那是因为编解码器没有消耗它们。常见的原因是它对编解码器特定的数据不满意(http://bigflake.com/mediacodec/#q3)。首先发送CODEC_CONFIG块,并使其看起来像来自编码器的内容,一切应该都会正常。或者至少会以不同的方式失败。:-| - fadden
1个回答

2

所以,我最终发现,即使我使用了输出表面,我仍然需要手动清空输出缓冲区。通过调用decoder.dequeueOutputBuffer,然后再调用decoder.releaseOutputBuffer,输入缓冲区就能按预期工作。

我还能够通过传递单个NAL单元以及完整的访问单元(每个PES数据包一个)来获取输出,但是通过传递完整的访问单元可以获得最清晰的视频。


你好,你有一个可用的例子吗?我一直在尝试做同样的事情但是没有成功。先谢谢了! - Jan Rozenbajgier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接