我在此处有一个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
日志再也没有出现过一次。我想念什么或做错什么了?
答案 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个视频的元数据格式错误,因此修复它们将花费大量时间和金钱。相反,我们想找到一个播放器方面的解决方案。这是我的能力:
HlsExtractorFactory
传递到HlsMediaSource.Factory
实例:HlsMediaSource hlsMediaSource = = new HlsMediaSource.Factory(dataSourceFactory)
.setExtractorFactory(new HlsExtractorFactoryProxy())
.createMediaSource(Uri.parse("https://vcloud.blueframetech.com/file/hls/13836.m3u8"));
我无法扩展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
,则用其自己的自定义版本TsExtractor
(TsExtractor
)替换。
要创建此自定义TsExtractorProxy
,我从the DefaultHlsExtractorFactory
class复制了TsExtractorProxy
方法的全部内容,并更改了一条语句:
createTsExtractor
new TsExtractor(
TsExtractor.MODE_HLS,
timestampAdjuster,
new DefaultTsPayloadReaderFactory(payloadReaderFactoryFlags, muxedCaptionFormats));
new TsExtractor(
TsExtractor.MODE_HLS,
timestampAdjuster,
new TsPayloadReaderFactoryProxy(payloadReaderFactoryFlags, muxedCaptionFormats));
代理如上所述,我需要在此处创建一个代理。这个公开了两个公共方法:TsPayloadReaderFactory
和createInitialPayloadReaders
。我只需要调整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
Id3ReaderProxy
代理此类具有 五个 公共方法,但仅需要调整以下一种方法:Id3Reader
。我没有传递packetStarted
参数,而是用flags
TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR
完成所有这些艰苦的工作后,尽管ID3标签被破坏了,我现在仍可以获取元数据事件