Отображение видео h264 из потока mpegts через udp: / / на android
Отображение видео h264 из потока mpegts через udp: / / на android.
Я уже несколько дней пытаюсь заставить это работать, но безуспешно. То, что у меня есть, - это устройство, которое производит видеопоток h264, который он многоадресно передает в контейнере mpegts по raw udp (не rtp). Я пытаюсь сделать так, чтобы это отображалось в пользовательском приложении на android.
Я читал, что встроенный в Android MediaPlayer поддерживает как H264 (avc), так и mpegts, но он не обрабатывает udp: / / потоки, поэтому я не может использовать это (что было бы намного проще). Вместо этого я попытался вручную разобрать поток mpegts в элементарный поток и передать его в MediaCodec, который был передан поверхности SurfaceView. Независимо от того, что я, кажется, пытаюсь, всегда происходят две вещи (как только я исправляю исключения и т. д.):
- вид поверхности всегда черный.
- MediaCodec всегда принимает около 6-9 буферов, а затем
dequeueInputBufferпросто начинает мгновенно отказывать (возвращая -1), и я не могу встать в очередь что-нибудь еще.
Я могу разделить поток mpeg на пакеты TS, а затем объединить их полезные нагрузки в пакеты PES. Я пробовал передавать полные пакеты PES (за вычетом заголовка PES) в MediaCodec.
Я также попытался разделить пакеты PES на отдельные конечные единицы, разделив их на x00x00x01 и передавая их по отдельности в MediaCodec.
Я также пытался воздержаться от передачи в NAL unit, пока не получу SPS NAL unit и не передам его первым с помощью BUFFER_FLAG_CODEC_CONFIG.
Все это приводит к тому же самому, о чем говорилось выше. У меня нет идей о том, что попробовать, так что любая помощь будет очень признательна.
Некоторые моменты, в которых я все еще не уверен:
Почти все примеры, которые я видел, получают Медиаформат из MediaExtractor,который я не могу использовать в потоке. Те немногие, которые не используют эксплицитность MediaExtractor, устанавливают csd-0 и csd-1 из bytestrings, которые не объясняются. Я читал, что пакет SPS можно поместить в буфер вместо этого, так что это то, что я пытался.
Я не знаю, что передать в presentationTimeUs. Пакеты TS имеют PCR, а пакеты PES - PTS, но я не знаю, чего ожидает api и как они связаны.
Я не уверен, как данные должны быть переданы в MediaCodec (это поэтому он перестает давать мне буферы?). У меня появилась идея передать отдельные конечные единицы из этого так называемого поста:
декодирование необработанного потока H264 в андроид?
Другие ссылки, которые я использовал для этого примера:
Код (извините, он довольно длинный):
Я только что создал тестовое приложение из базового шаблона в 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. Они передаются в функцию parse:
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);
}
}
}
затем перешли к процессу. функция:
// 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;
}
}
2 ответов:
Таким образом, я в конце концов понял, что, хотя я и использовал выходную поверхность, мне пришлось вручную сливать выходные буферы. Вызывая
decoder.dequeueOutputBufferи затемdecoder.releaseOutputBuffer, входные буферы работали так, как ожидалось.Я также мог получать выходные данные, передавая как отдельные конечные единицы, так и полные единицы доступа (по одной на пакет PES), но я получил самое четкое видео, передавая полные единицы доступа.
У вас есть полный код проводки? Я отчаянно пытаюсь решить ту же проблему, и вы кодируете публикацию выше, пропуская несколько важных деталей.
Правка:
Согласен, это комментарий, но я не могу комментировать этот сайт, потому что я недостаточно крут. =(
Моей главной проблемой было то, что я забыл позвонить .clear () на входных буферах, и поэтому я получал несколько кадров, а затем мусор, когда встроенные буферы начинали использоваться повторно.Чего не хватает, так это деталей поиск конфигурационных данных pps и sps, в частности, утилиты.реализация indexOf ().
Вот мое решение, используя Mono.Net / Xamarin в c#:
Https://github.com/jasells/Droid-Vid
Хотя я еще не реализовал динамическое расположение pps и sps из потока, у меня есть идея, с чего начать (искать 0x0, 0x0, 0x0, 0x1, 0x?? стартовый код данных pps). Ваша реализация будет интересна мне в этом уважение.
Спасибо!
Comments