Visualización de vídeo h264 desde un flujo de mpegts sobre upd: // en android

Visualización de vídeo h264 desde una secuencia de mpegts sobre upd: // en android.

He estado intentando durante unos días conseguir que esto funcione sin éxito. Lo que tengo es un dispositivo que produce un flujo de vídeo h264 que se multicasts en un contenedor mpegts sobre udp crudo (no rtp). Estoy intentando conseguir esto para mostrar en una aplicación personalizada en android.

He leído que androide incorporado en MediaPlayer soporta h264 (avc) y mpegts, pero que no maneja udp: // streams, así que no puedo usar eso (que sería por mucho el más simple). En su lugar, he intentado analizar manualmente el flujo de mpegts en un flujo elemental y pasarlo a un MediaCodec que se ha pasado la superficie de un SurfaceView. No importa lo que parezca intentar, dos cosas siempre suceden (una vez que arreglar las excepciones, etc):

  • El SurfaceView siempre es negro.
  • El MediaCodec siempre acepta alrededor de 6-9 buffers y luego dequeueInputBuffer comienza inmediatamente fallando (devolviendo -1) y no puedo hacer cola nada más.

Puedo dividir el flujo mpeg en paquetes TS y luego unir sus cargas en paquetes PES. He intentado pasar paquetes PES completos (menos el encabezado PES) en MediaCodec.

También he intentado dividir los paquetes PES en unidades NAL individuales dividiendo en \x00\x00\x01 y pasarlos individualmente en el MediaCodec.

También he intentado retener en pasar en la unidad NAL hasta que he recibido la unidad SPS NAL y pasando eso primero con BUFFER_FLAG_CODEC_CONFIG .

Todos estos resultados en la misma cosa mencionada anteriormente. Estoy sin ideas sobre qué probar, por lo que cualquier ayuda sería muy apreciada.

Algunos puntos que todavía no estoy seguro acerca de:

  • Casi todos los ejemplos que he visto obtener el MediaFormat de MediaExtractor, que no puedo usar en la corriente. Los pocos que no utilizan la explicación de MediaExtractor establecen csd-0 y csd-1 de bytestrings que no se explican. He leído que el paquete SPS se puede poner en el búfer en su lugar por lo que es lo que he intentado.

  • No estoy seguro de qué pasar en presentationTimeUs. Los paquetes TS tienen un PCR y los paquetes PES tienen un PTS, pero no sé lo que se espera por el api y cómo se relacionan.

  • No estoy seguro de cómo los datos deben ser pasados ​​a MediaCodec (es por eso que deja de darme buffers?). Tengo la idea de pasar en unidades NAL individuales de este modo: Decodificación Raw H264 corriente en android?

Otras referencias he utilizado para hacer este ejemplo:

  • Formato MPEG-TS
  • Formato PES
  • Formato PES

Código (lo siento es bastante largo):

Acabo de crear una aplicación de prueba de la plantilla básica en AndroidStudio, la mayor parte de él es boilerplate así que voy a pegar el material relacionado con el vídeo.

SurfaceView se define en el xml, así que agarra y consigue la superficie cuando se crea / cambia

 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 es una clase interna que lee datos de un puerto de multidifusión y lo pasa a una función de análisis en un subproceso de fondo:

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

La recepción funciona bien, cada paquete de datagrama contiene dos paquetes TS. Se pasan a la función de análisis:

  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 paquetes son analizados por la clase 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); } } } 

Luego se pasa a la función 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; } } 

Los paquetes PES son analizados por la clase 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; } } 

2 Solutions collect form web for “Visualización de vídeo h264 desde un flujo de mpegts sobre upd: // en android”

Así que finalmente me di cuenta de que, a pesar de que estaba usando una superficie de salida, tuve que drenar manualmente los búferes de salida. Llamando decoder.dequeueOutputBuffer y decoder.releaseOutputBuffer , los buffers de entrada funcionaron como se esperaba.

También pude obtener salida pasando tanto en unidades NAL individuales como unidades de acceso completo (una por paquete PES), pero obtuve el video más claro pasando en unidades de acceso completo.

¿Tiene una publicación de código completo? Estoy desesperadamente tratando de resolver el mismo problema y la publicación de código anterior falta algunos detalles críticos.

editar:

De acuerdo, esto es un comentario, pero no puedo comentar en este sitio porque no soy lo suficientemente fría. = (

Mi problema principal era que me olvidé de llamar a .clear () en los búferes de entrada, y así conseguiría unos cuantos cuadros, y luego basura cuando los buffers comenzaran a ser reutilizados.

Lo que falta son los detalles de la búsqueda de los datos de configuración de pps y sps, específicamente, la implementación Utility.indexOf ().

Aquí está mi solución, usando Mono.Net / Xamarin en c #:

https://github.com/jasells/Droid-Vid

Sin embargo, todavía no he implementado la ubicación dinámica de pps y sps de la secuencia, tengo una idea de por dónde empezar (busque el 0x0, 0x0, 0x0, 0x1, 0x ?? código de inicio de los datos pps). Su implementación me interesaría a este respecto.

¡Gracias!

  • MediaCodec con entrada de superficie: Grabación en segundo plano
  • No puedo capturar la pantalla de Android con la herramienta de registro de pantalla adb
  • Screen Recorder Plugin para Android en Unity
  • MediaCodec KEY_FRAME_RATE parece ser ignorado
  • Cómo salvar SurfaceTexture como bitmap
  • PCM -> AAC (Encoder) -> PCM (Decodificador) en tiempo real con optimización correcta
  • Cómo jugar raw h264 producido por MediaCodec encoder?
  • ¿Cómo inicializar MediaFormat para configurar un MediaCodec para decodificar datos AAC sin procesar?
  • MediaCodec createInputSurface
  • ¿Cómo pasar la vista previa de la cámara a la superficie creada por MediaCodec.createInputSurface ()?
  • ¿Qué significa el código de error -1010 en Android MediaCodec?
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.