如何在ExoPlayer中从HLS流中提取定时的ID3元数据?

时间:2019-05-23 19:14:00

标签: java android exoplayer

我在此处有一个M3U8文件:https://vcloud.blueframetech.com/file/hls/13836.m3u8

该视频每秒钟包含定时的元数据。我的目标是从ExoPlayer读取此元数据。我的MainActivity.java中目前有以下内容:

package com.test.exoplayermetadatatest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;

import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.util.Util;

public class MainActivity extends AppCompatActivity implements MetadataOutput, Player.EventListener
{

    @Override
    protected void onCreate ( Bundle savedInstanceState )
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Context context = getApplicationContext();

        SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(context);

        PlayerView view = findViewById(R.id.player);

        view.setPlayer(player);

        DataSource.Factory dataSourceFactory =
            new DefaultHttpDataSourceFactory(Util.getUserAgent(context, "app-name"));

        HlsMediaSource hlsMediaSource =
            new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse("https://vcloud.blueframetech.com/file/hls/13836.m3u8"));

        player.addMetadataOutput(this);
        player.addListener(this);

        player.prepare(hlsMediaSource);

        player.setPlayWhenReady(true);
    }

    @Override
    public void onTracksChanged( TrackGroupArray trackGroups, TrackSelectionArray trackSelections)
    {
        for ( int i = 0; i < trackGroups.length; i++ )
        {
            TrackGroup trackGroup = trackGroups.get(i);
            for ( int j = 0; j < trackGroup.length; j++ )
            {
                Metadata trackMetadata = trackGroup.getFormat(j).metadata;
                if ( trackMetadata != null )
                {
                    Log.d("METADATA TRACK", trackMetadata.toString());
                }
            }
        }
    }

    @Override
    public void onMetadata ( Metadata metadata )
    {
        Log.d("METADATA", metadata.toString());
    }

}

加载应用程序后,我看到METADATA TRACK日志只出现一次,但是METADATA日志再也没有出现过一次。我想念什么或做错什么了?

1 个答案:

答案 0 :(得分:0)

我在这里有一个很长的答案...

问题

因此,首先,我注意到 我的确切解决方案 在ExoPlayer 2.1.1中有效,但在2.10.1中无效。这使我认为ID3元数据存在回归,因此我通过GitHub与Google联系。他们很快做出回应,并注意到我的视频中的元数据实际上存在问题。对于每个数据包{ID1标签的开始data_alignment_indicator位应该为1,对于每个数据包0都是先前的ID3标签的延续(如果是ID3,则为0)标签太大,无法容纳单个标签的64 KB限制。对于我们的内容,该位 始终 设置为0-表示任何地方都没有“ ID3标签的开始”。

较旧版本的ExoPlayer并未对此进行检查,因此无法正确支持超过64 KB的元数据。新版本 对此进行了检查,但无法阅读我们的破损视频


解决方案

显然 正确 的答案是修复我们的内容,但是我们有超过100,000个视频的元数据格式错误,因此修复它们将花费大量时间和金钱。相反,我们想找到一个播放器方面的解决方案。这是我的能力:

1。将自定义HlsExtractorFactory传递到HlsMediaSource.Factory实例:

HlsMediaSource hlsMediaSource = = new HlsMediaSource.Factory(dataSourceFactory)
    .setExtractorFactory(new HlsExtractorFactoryProxy())
    .createMediaSource(Uri.parse("https://vcloud.blueframetech.com/file/hls/13836.m3u8"));

2。创建一个自定义HlsExtractorFactory

我无法扩展DefaultHlsExtractorFactory,也不想从头开始实现自己的提取器工厂,所以我选择了Proxy Pattern

    public class HlsExtractorFactoryProxy implements HlsExtractorFactory
    {

        private DefaultHlsExtractorFactory internal = new DefaultHlsExtractorFactory();

        @Override
        public HlsExtractorFactory.Result createExtractor (
            Extractor previousExtractor,
            Uri uri,
            Format format,
            List<Format> muxedCaptionFormats,
            DrmInitData drmInitData,
            TimestampAdjuster timestampAdjuster,
            Map<String, List<String>> responseHeaders,
            ExtractorInput extractorInput
        )
            throws InterruptedException, IOException
        {
            HlsExtractorFactory.Result result = internal.createExtractor(
                previousExtractor,
                uri,
                format,
                muxedCaptionFormats,
                drmInitData,
                timestampAdjuster,
                responseHeaders,
                extractorInput
            );

            if ( result.extractor instanceof TsExtractor )
            {
                return createNewTsExtractor(
                    0,
                    true,
                    format,
                    muxedCaptionFormats,
                    timestampAdjuster
                );
            }

            return result;
        }

        private HlsExtractorFactory.Result createNewTsExtractor (
            @DefaultTsPayloadReaderFactory.Flags int userProvidedPayloadReaderFactoryFlags,
            boolean exposeCea608WhenMissingDeclarations,
            Format format,
            List<Format> muxedCaptionFormats,
            TimestampAdjuster timestampAdjuster
        )
        {
            @DefaultTsPayloadReaderFactory.Flags
            int payloadReaderFactoryFlags =
                DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM
                | userProvidedPayloadReaderFactoryFlags;
            if ( muxedCaptionFormats != null )
            {
                // The playlist declares closed caption renditions, we should ignore descriptors.
                payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS;
            }
            else if ( exposeCea608WhenMissingDeclarations )
            {
                // The playlist does not provide any closed caption information. We preemptively declare a
                // closed caption track on channel 0.
                muxedCaptionFormats =
                    Collections.singletonList(
                        Format.createTextSampleFormat(
                            null,
                            MimeTypes.APPLICATION_CEA608,
                            0,
                            null
                        ));
            }
            else
            {
                muxedCaptionFormats = Collections.emptyList();
            }
            String codecs = format.codecs;
            if ( !TextUtils.isEmpty(codecs) )
            {
                // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
                // exist. If we know from the codec attribute that they don't exist, then we can
                // explicitly ignore them even if they're declared.
                if ( !MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs)) )
                {
                    payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM;
                }
                if ( !MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs)) )
                {
                    payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM;
                }
            }

            TsExtractor extractor = new TsExtractor(
                TsExtractor.MODE_HLS,
                timestampAdjuster,
                new TsPayloadReaderFactoryProxy(payloadReaderFactoryFlags, muxedCaptionFormats)
            );

            return new HlsExtractorFactory.Result(
                extractor,
                false,
                true
            );
        }

    }

每个类HlsExtractorFactory仅公开一个公共方法:createExtractor。此方法运行DefaultHlsExtractorFactory的{​​{1}}方法,如果产生了createExtractor,则用其自己的自定义版本TsExtractorTsExtractor)替换。

要创建此自定义TsExtractorProxy,我从the DefaultHlsExtractorFactory class复制了TsExtractorProxy方法的全部内容,并更改了一条语句:

createTsExtractor
new TsExtractor(
        TsExtractor.MODE_HLS,
        timestampAdjuster,
new DefaultTsPayloadReaderFactory(payloadReaderFactoryFlags, muxedCaptionFormats));

3。创建new TsExtractor( TsExtractor.MODE_HLS, timestampAdjuster, new TsPayloadReaderFactoryProxy(payloadReaderFactoryFlags, muxedCaptionFormats)); 代理

如上所述,我需要在此处创建一个代理。这个公开了两个公共方法:TsPayloadReaderFactorycreateInitialPayloadReaders。我只需要调整createPayloadReader

的实现
createPayloadReader

正如您在此处更清楚地看到的那样,当处理类型为 public class TsPayloadReaderFactoryProxy implements TsPayloadReader.Factory { private DefaultTsPayloadReaderFactory internal; public TsPayloadReaderFactoryProxy(int payloadReaderFactoryFlags, List<Format> muxedCaptionFormats) { internal = new DefaultTsPayloadReaderFactory(payloadReaderFactoryFlags, muxedCaptionFormats); } @Override public SparseArray<TsPayloadReader> createInitialPayloadReaders () { return internal.createInitialPayloadReaders(); } @Override public TsPayloadReader createPayloadReader ( int streamType, TsPayloadReader.EsInfo esInfo ) { if ( streamType == TsExtractor.TS_STREAM_TYPE_ID3) { return new PesReader(new Id3ReaderProxy()); } else { return internal.createPayloadReader(streamType, esInfo); } } } 的流而不是实例化TsExtractor.TS_STREAM_TYPE_ID3时,我将实例化一个Id3Reader

4。创建Id3ReaderProxy代理

此类具有 五个 公共方法,但仅需要调整以下一种方法:Id3Reader。我没有传递packetStarted参数,而是用flags

覆盖了它。
TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR

完成所有这些艰苦的工作后,尽管ID3标签被破坏了,我现在仍可以获取元数据事件