ExoPlayer备忘录

ExoPlayer


1.简述与应用范围

ExpPlayer是一个开源的,App等级的媒体API,它的开源项目包含了library和示例。

ExoPlayer相较于MediaPlayer有很多优点:

1. 支持基于http的移动流媒体协议,包括DASH,HSL,Smooth Stream。同时也支持文件流和udp流等。

2. 支持更多媒体封装格式,包括mp4,mp3,Webm,aac,mkv,mpeg-ts。

3. 支持DRM(Digital Right Management 数字版权管理)。

4. 支持HD高清播放。

5. 支持自定义和拓展使用场景。

2.上层调用方式

(本节说明重点为demo。)

简单来说,上层调用方式基本为:

PlayerActivity -> DemoPlayer -> ExoPlayer 

PlayerActivity -> RendererBuilder -> ExtractorRendererBuilder

类图为:

其中PlayerActivity面向UI层,一方面控制了播放器DemoPlayer,一方面选择了Renderer。

这里的Renderer指定了数据源格式、解码方式和缓冲区大小等。(说明,这里的缓冲区大小指RollingSampleBuffer的大小,不会影响进入播放的速度,只会影响缓存数据的最大值)

ExoPlayer则是媒体API接口。

DemoPlayer中直接封装了ExoPlayer和相关回调接口,负责播放器的逻辑控制和传入SurfaceView等操作,而非播放器的内部原理。

这里通过时序图来说明Demo中几个类的调用和封装方式。

3.代码结构

简单来说,代码结构是这样:

ExoPlayer ->ExoPlayerImpl -> ExoPlayerImplInternal -> TrackRenderer 

MediaCodecVideoTrackRenderer & MediaCodecAudioTrackRenderer ->  MediaCodecTrackRenderer -> SampleSourceTrackRenderer -> SampleSource,SampleSourceReader 

ExtractorSampleSource -> DataSource & Extractor & Loader

这里,ExoPlayer为接口。ExoPlayerImpl为实现,实现的一些详细步骤在ExoPlayerImplInternal中。后者用Handler消息机制进行异步通信,必要时会阻塞。

TrackRenderer是渲染器接口。

MediaCodecTrackRenderer中加入了MediaCodec(Android硬解码)。这里能看出,ExoPlayer用的是硬解,并且要求4.1以上Android系统。

SampleSourceTrackRenderer中调用了SampleSource,SampleSourceReader接口。SampleSource在这里指的是解封装后的媒体数据。

ExtractorSampleSource相当于一个核心控制器,它实现了SampleSource和SampleSourceReader接口。它通过实际的控制线程Loader,把从某DataSource即数据源中传过来的原始数据,传递给某Extractor来解封装。原始数据解析成SampleSource后,储存在RollingSampleBuffer即环形缓冲区中。

MediaCodecTrackRenderer会间接通过ExtractorSampleSource间接从RollingSampleBuffer中读取数据并渲染成画面,显示到SurfaceView中。

最后的过程有些复杂,流程图如下所示:

4.代码原理

1.ExoPlayer -> ExoPlayerImpl -> ExoPlayerImplInternal

通过以下这段ExoPlayerImpl的构造方法代码,可以看出来ExoPlayerImpl中持有一个ExoPlayerImplInternal对象来控制播放器。创建ExoPlayerImplInternal对象时传入了一个eventHandler对象,把底层的错误信息和状态改变信息传递给上层处理。

ExoPlayerImpl类中构造方法:

eventHandler = new Handler() {
  @Override
  public void handleMessage(Message msg) {
    ExoPlayerImpl.this.handleEvent(msg);
  }
};
internalPlayer = new ExoPlayerImplInternal(eventHandler, playWhenReady, selectedTrackIndices,
    minBufferMs, minRebufferMs);

具体的功能性代码块,都在ExoPlayerImplInternal中实现。

状态改变信息和错误信息会通过eventHandler传上来进行处理。

ExoPlayerImpl类:

// Not private so it can be called from an inner class without going through
// a thunk method.
/* package */ void handleEvent(Message msg) {
    switch (msg.what) {
    case ExoPlayerImplInternal.MSG_PREPARED: {
        System.arraycopy(msg.obj, 0, trackFormats, 0, trackFormats.length);
        playbackState = msg.arg1;
        for (Listener listener : listeners) {
            listener.onPlayerStateChanged(playWhenReady, playbackState);
        }
        break;
    }
    case ExoPlayerImplInternal.MSG_STATE_CHANGED: {
        playbackState = msg.arg1;
        for (Listener listener : listeners) {
            listener.onPlayerStateChanged(playWhenReady, playbackState);
        }
        break;
    }
    case ExoPlayerImplInternal.MSG_SET_PLAY_WHEN_READY_ACK: {
        pendingPlayWhenReadyAcks--;
        if (pendingPlayWhenReadyAcks == 0) {
            for (Listener listener : listeners) {
                listener.onPlayWhenReadyCommitted();
            }
        }
        break;
    }
    case ExoPlayerImplInternal.MSG_ERROR: {
        ExoPlaybackException exception = (ExoPlaybackException) msg.obj;
        for (Listener listener : listeners) {
            listener.onPlayerError(exception);
        }
        break;
    }
    }
}

这里的listeners是一个CopyOnWriteArrayList,里面的对象都是Listener,这里用的是一个观察者模式,用于给上层监听回调消息。上层即DemoPlayer或是EventLogger都在这里注册或注销监听。

2.ExoPlayerImplInternal -> TrackRenderer -> SampleSource,SampleSourceReader -> ExtractorSampleSource

1)ExoPlayerImplInternal中消息机制

    ExoPlayerImplInternal类中构造方法:

    internalPlaybackThread = new PriorityHandlerThread(getClass().getSimpleName() + ":Handler",
            Process.THREAD_PRIORITY_AUDIO);
    internalPlaybackThread.start();
    handler = new Handler(internalPlaybackThread.getLooper(), this);

ExoPlayerImplInternal实现了Handler.Callback接口:

ExoPlayerImplInternal类:

@Override
public boolean handleMessage(Message msg) {
    try {
        switch (msg.what) {
        case MSG_PREPARE: {
            prepareInternal((TrackRenderer[]) msg.obj);
            return true;
        }
        case MSG_INCREMENTAL_PREPARE: {
            incrementalPrepareInternal();
            return true;
        }
        case MSG_SET_PLAY_WHEN_READY: {
            setPlayWhenReadyInternal(msg.arg1 != 0);
            return true;
        }
        case MSG_DO_SOME_WORK: {
            doSomeWork();
            return true;
        }
        case MSG_SEEK_TO: {
            seekToInternal(Util.getLong(msg.arg1, msg.arg2));
            return true;
        }
        case MSG_STOP: {
            stopInternal();
            return true;
        }
        case MSG_RELEASE: {
            releaseInternal();
            return true;
        }
        case MSG_CUSTOM: {
            sendMessageInternal(msg.arg1, msg.obj);
            return true;
        }
        case MSG_SET_RENDERER_SELECTED_TRACK: {
            setRendererSelectedTrackInternal(msg.arg1, msg.arg2);
            return true;
        }
        default:
            return false;
        }
    } catch (ExoPlaybackException e) {
        Log.e(TAG, "Internal track renderer error.", e);
        eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
        stopInternal();
        return true;
    } catch (RuntimeException e) {
        Log.e(TAG, "Internal runtime error.", e);
        eventHandler.obtainMessage(MSG_ERROR, new ExoPlaybackException(e, true)).sendToTarget();
        stopInternal();
        return true;
    }
}

通过这段代码,可以看出来,在ExoPlayerImplInternal内部是通过消息来控制播放器逻辑(控制TrackRenderer)。

2)doSomeWork分析及作用

ExoPlayerImplInternal类:

private void doSomeWork() throws ExoPlaybackException {
    TraceUtil.beginSection("doSomeWork");
    long operationStartTimeMs = SystemClock.elapsedRealtime();
    long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME_US ? durationUs : Long.MAX_VALUE;
    boolean allRenderersEnded = true;
    boolean allRenderersReadyOrEnded = true;
    updatePositionUs();// 笔记:更新positionUs
    for (int i = 0; i < enabledRenderers.size(); i++) {
        TrackRenderer renderer = enabledRenderers.get(i);
        // TODO: Each renderer should return the maximum delay before which
        // it wishes to be
        // invoked again. The minimum of these values should then be used as
        // the delay before the next
        // invocation of this method.

        // 笔记:这里调用了renderer的doSomeWork方法并传入了positionUs,
        //      elapsedRealtimeUs是个独立的系统时间参考
        renderer.doSomeWork(positionUs, elapsedRealtimeUs);
        allRenderersEnded = allRenderersEnded && renderer.isEnded();

        // Determine whether the renderer is ready (or ended). If it‘s not,
        // throw an error that‘s
        // preventing the renderer from making progress, if such an error
        // exists.
        boolean rendererReadyOrEnded = rendererReadyOrEnded(renderer);
        if (!rendererReadyOrEnded) {
            renderer.maybeThrowError();
        }
        allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded;

        if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {
            // We‘ve already encountered a track for which the buffered
            // position is unknown. Hence the
            // media buffer position unknown regardless of the buffered
            // position of this track.
        } else {
            long rendererDurationUs = renderer.getDurationUs();
            long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
            if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {
                bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US;
            } else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US
                    || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US
                            && rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
                            && rendererBufferedPositionUs >= rendererDurationUs)) {
                // This track is fully buffered.
            } else {
                bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
            }
        }
    }

    // 笔记:更新缓冲位置,主要用于上层回调
    this.bufferedPositionUs = bufferedPositionUs;

    // 笔记:根据durationUs和positionUs来判断状态和开关渲染器(Renderer)
    if (allRenderersEnded && (durationUs == TrackRenderer.UNKNOWN_TIME_US || durationUs <= positionUs)) {
        setState(ExoPlayer.STATE_ENDED);
        stopRenderers();
    } else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded) {
        setState(ExoPlayer.STATE_READY);
        if (playWhenReady) {
            startRenderers();
        }
    } else if (state == ExoPlayer.STATE_READY && !allRenderersReadyOrEnded) {
        rebuffering = playWhenReady;
        setState(ExoPlayer.STATE_BUFFERING);
        stopRenderers();
    }

    // 笔记:准备再次调用doSomework
    handler.removeMessages(MSG_DO_SOME_WORK);
    if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {
        scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, RENDERING_INTERVAL_MS);
    } else if (!enabledRenderers.isEmpty()) {
        scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, IDLE_INTERVAL_MS);
    }

    TraceUtil.endSection();
}

private void scheduleNextOperation(int operationType, long thisOperationStartTimeMs, long intervalMs) {
    long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;
    long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();
    if (nextOperationDelayMs <= 0) {
        handler.sendEmptyMessage(operationType);
    } else {
        handler.sendEmptyMessageDelayed(operationType, nextOperationDelayMs);
    }
}

// 笔记:通过上层传入的eventHandler把状态改变信息传递给上层
private void setState(int state) {
    if (this.state != state) {
        this.state = state;
        eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget();
    }
}

doSomeWork方法是在播放器执行完prepare后执行的。是在准备动作都完成后,具体控制播放器开始渲染画面的方法。

在以上代码中我们可以看出来,这里完成的主要动作有:

1. 更新positionUs(以及elapsedRealtimeUs)
2. renderer.doSomeWork
3. 把播放状态回调上层
4. 定时执行下一次doSomeWork

3)updataPositionUs和renderer.doSomeWork分析

positionUs指的是实际渲染位置。

ExoPlayerImplInternal类:

private void updatePositionUs() {
    if (rendererMediaClock != null && enabledRenderers.contains(rendererMediaClockSource)
            && !rendererMediaClockSource.isEnded()) {
        positionUs = rendererMediaClock.getPositionUs();
        standaloneMediaClock.setPositionUs(positionUs);
    } else {
        positionUs = standaloneMediaClock.getPositionUs();
    }
    elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
}

通过这段在ExoPlayerImplInternal类中的代码,我们看出,这有两个分支,第一个分支主要是用于有音频的情况下,音频时间可以作为整体参考时间,来调整positionUs。第二个分支是没有音频的情况下,用系统独立时钟作为整体参考时间,来调整positionUs。

MediaCodecTrackRenderer类:

@Override
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
    // 笔记:判断是否应该继续缓冲
    sourceState = continueBufferingSource(positionUs)
            ? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState) : SOURCE_STATE_NOT_READY;
    // 笔记:判断解码是否连续,如果不连续,则重启解码器
    checkForDiscontinuity(positionUs);
    if (format == null) {
        // 笔记:读取格式
        readFormat(positionUs);
    }
    if (codec == null && shouldInitCodec()) {
        // 笔记:当有格式无解码器时,开启解码器
        maybeInitCodec();
    }
    if (codec != null) {
        TraceUtil.beginSection("drainAndFeed");
        // 笔记:如果解码器中可以输出缓冲,则会返回true,否则返回false
        while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {
        }
        // 笔记:如果解码器还可以输入原始帧,则返回true,否则返回false,第二个参数代表是否首次执行
        if (feedInputBuffer(positionUs, true)) {
            while (feedInputBuffer(positionUs, false)) {
            }
        }
        TraceUtil.endSection();
    }
    codecCounters.ensureUpdated();
}

positionUs传递给了drainOutputBuffer方法和feedInputBuffer方法。用于调整播放时间,和获取缓冲帧。

drainOutputBuffer方法调用到了processOutputBuffer方法,这里处理缓冲帧。这个方法在MediaCodecTrackRenderer类中是个抽象方法,具体实现在MediaCodecVideoTrackRenderer和MediaCodecAudioTrackRenderer类中。

MediaCodecVideoTrackRenderer类:

// 笔记:返回true意味着输出的缓冲帧已经被渲染,false意味着尚未被渲染
@Override
protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, ByteBuffer buffer,
        MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) {
    if (shouldSkip) {
        skipOutputBuffer(codec, bufferIndex);
        return true;
    }

    if (!renderedFirstFrame) {
        if (Util.SDK_INT >= 21) {
            renderOutputBufferV21(codec, bufferIndex, System.nanoTime());
        } else {
            renderOutputBuffer(codec, bufferIndex);
        }
        return true;
    }

    if (getState() != TrackRenderer.STATE_STARTED) {
        return false;
    }

    // Compute how many microseconds it is until the buffer‘s presentation
    // time.
    long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs;
    long earlyUs = bufferInfo.presentationTimeUs - positionUs - elapsedSinceStartOfLoopUs;

    // Compute the buffer‘s desired release time in nanoseconds.
    long systemTimeNs = System.nanoTime();
    long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000);

    // Apply a timestamp adjustment, if there is one.
    long adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime(bufferInfo.presentationTimeUs,
            unadjustedFrameReleaseTimeNs);
    earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;

    // 笔记:以上是通过positionUs(实际渲染位置),elapsedRealtimeUs(独立时钟位置),
    //      bufferInfo.presentationTimeUs(缓冲帧位置)得出缓冲位置和播放位置之间的时间差值。

    // 笔记:如果渲染位置在此缓冲帧位置后面30ms,则弃掉此帧
    if (earlyUs < -30000) {
        // We‘re more than 30ms late rendering the frame.
        dropOutputBuffer(codec, bufferIndex);
        return true;
    }

    if (Util.SDK_INT >= 21) {
        // 笔记:如果系统api在21以上,则可以在framework层控制渲染速度
        // Let the underlying framework time the release.
        // 笔记:如果渲染位置在缓冲帧位置50毫秒之前,就return false。否则则渲染。
        if (earlyUs < 50000) {
            renderOutputBufferV21(codec, bufferIndex, adjustedReleaseTimeNs);
            return true;
        }
    } else {
        // 笔记:如果系统api在21以下,我们需要自己控制渲染速度
        // We need to time the release ourselves.
        if (earlyUs < 30000) {
            // 笔记:如果渲染位置和缓冲帧位置之差在30毫秒和11毫秒之间,则推迟至少1毫秒再渲染。
            //      如果在11毫秒以内,则直接渲染。
            if (earlyUs > 11000) {
                // We‘re a little too early to render the frame. Sleep until
                // the frame can be rendered.
                // Note: The 11ms threshold was chosen fairly arbitrarily.
                try {
                    // Subtracting 10000 rather than 11000 ensures the sleep
                    // time will be at least 1ms.
                    Thread.sleep((earlyUs - 10000) / 1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            renderOutputBuffer(codec, bufferIndex);
            return true;
        }
    }

    // We‘re either not playing, or it‘s not time to render the frame yet.
    // 笔记:return false的意思是,我们既不播放,而且也不渲染这帧。
    return false;
}

在renderOutputBuffer中,

codec.releaseOutputBuffer(bufferIndex, true);

通过releaseOutputBuffer方法把相关帧播放到surface中。

以上是通过positionUs调整缓冲时间以及播放缓冲帧的代码。

在feedInputBuffer中,

result = readSource(positionUs, formatHolder, sampleHolder, false);

通过readSource,调用到了ExtractorSampleSource中的readData方法,从rollingBuffer中取到了数据。

这是通过positionUs获取缓冲帧的代码。

通过这些代码可以分析出,如果positionUs获取错误的话,那么会直接影响到播放流程中从缓冲区获取数据和解码器渲染数据等功能。

3.ExtractorSampleSource -> DataSource & Extractor & Loader

1)ExtractingLoadable分析

ExtractingLoadable是一个ExtractorSampleSource中的内部类。它实现了Loadable接口。Loadable接口应用于Loader,后者是一个异步线程。在这里主要用于从DataSource数据源中获取数据放进RollingSampleBuffer即缓冲区中。

/**
 * Loads the media stream and extracts sample data from it.
 */
private static class ExtractingLoadable implements Loadable {

    private final Uri uri;
    private final DataSource dataSource;
    private final ExtractorHolder extractorHolder;
    private final Allocator allocator;
    private final int requestedBufferSize;
    private final PositionHolder positionHolder;

    private volatile boolean loadCanceled;

    private boolean pendingExtractorSeek;

    public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, Allocator allocator,
            int requestedBufferSize, long position) {
        this.uri = Assertions.checkNotNull(uri);
        this.dataSource = Assertions.checkNotNull(dataSource);
        this.extractorHolder = Assertions.checkNotNull(extractorHolder);
        this.allocator = Assertions.checkNotNull(allocator);
        this.requestedBufferSize = requestedBufferSize;
        positionHolder = new PositionHolder();
        positionHolder.position = position;
        pendingExtractorSeek = true;
    }

    // 笔记:用于控制线程的关闭
    @Override
    public void cancelLoad() {
        loadCanceled = true;
    }

    @Override
    public boolean isLoadCanceled() {
        return loadCanceled;
    }

    @Override
    public void load() throws IOException, InterruptedException {
        int result = Extractor.RESULT_CONTINUE;
        while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
            ExtractorInput input = null;
            try {
                long position = positionHolder.position;
                // 笔记:开打数据源,这里C.LENGTH_UNBOUNDED值为-1
                long length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNBOUNDED, null));
                if (length != C.LENGTH_UNBOUNDED) {
                    length += position;
                }
                // 笔记:这里的ExtractorInput是一个对于数据源、读取位置、读取长度的封装
                //      用于向Extractor输入数据
                input = new DefaultExtractorInput(dataSource, position, length);
                // 笔记:通过数据选择正确的Extractor即文件封装拆解器
                Extractor extractor = extractorHolder.selectExtractor(input);
                if (pendingExtractorSeek) {
                    extractor.seek();
                    pendingExtractorSeek = false;
                }
                // 笔记:这个循环用于从Extractor中不断读取数据,放进RollingSampleBuffer中
                while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
                    allocator.blockWhileTotalBytesAllocatedExceeds(requestedBufferSize);
                    result = extractor.read(input, positionHolder);
                    // TODO: Implement throttling to stop us from buffering
                    // data too often.
                }
            } finally {
                if (result == Extractor.RESULT_SEEK) {
                    result = Extractor.RESULT_CONTINUE;
                } else if (input != null) {
                    positionHolder.position = input.getPosition();
                }
                // 笔记:关闭数据源
                dataSource.close();
            }
        }
    }

}

我们可以看出,线程中进行的主要动作是

1. dataSource.open,即打开数据源
2. Extractor extractor = extractorHolder.selectExtractor(input),选择正确的文件封装拆解器
3. result = extractor.read(input, positionHolder),从数据源中读取数据
4. dataSource.close,关闭数据源

2)ExtractorHolder分析

ExtractorHolder也是一个ExtractorSampleSource中的内部类。它主要负责持有Extractor。

    ExtractorHolder类:

    public Extractor selectExtractor(ExtractorInput input)
            throws UnrecognizedInputFormatException, IOException, InterruptedException {
        if (extractor != null) {
            return extractor;
        }
        for (Extractor extractor : extractors) {
            try {
                // 笔记:一旦识别到正确的解析器,则会返回true
                if (extractor.sniff(input)) {
                    this.extractor = extractor;
                    break;
                }
            } catch (EOFException e) {
                // Do nothing.
            }
            input.resetPeekPosition();
        }
        if (extractor == null) {
            throw new UnrecognizedInputFormatException(extractors);
        }
        // 笔记:这里调用了extractor.init即初始化
        extractor.init(extractorOutput);
        return extractor;
    }

3)Extractor分析

Extractor是个接口,表示文件封装解析器。里面主要有四个方法:

void init(ExtractorOutput output);

boolean sniff(ExtractorInput input) throws IOException, InterruptedException;

int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException;

void seek();

read方法是阻塞的。每次调用read只会获取一小部分数据。

同时这里定义了三个read方法的特殊返回值:

RESULT_CONTINUE = 0; //表示需要继续读取数据

RESULT_SEEK = 1; //表示需要重新定位数据

RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT; //表示已经读取结束

通过Extractor的实现类我们可以找到,当调用read方法时,都会调到trackOutput.sampleData方法。这个方法表示输出解封装后的帧。具体就是把解封装的帧存入RollingSampleBuffer中,在TrackOutput的实现类DefaultTrackOutput中的如下代码可以印证这一点:

@Override
public void sampleData(ParsableByteArray buffer, int length) {
    rollingBuffer.appendData(buffer, length);
}

具体的文件解封装这里不做细节分析。

4.其他

5.相关性补充

ijkplayer中Android部分:

ijkplayer是bilibili推出的同时支持ios和Android,硬解和软解的开源播放器框架。其中,在Android代码中,硬解部分应用了ExoPlayer,软解部分应用了ffmepg和sdl。

ijkplayer的demo中,调用方式是这样的:

VideoActivity -> IjkVideoView -> IMediaPlayer -> AbstractMediaPlayer 

AbstractMediaPlayer -> IjkExoMediaPlayer -> DemoPlayer -> ExoPlayer 

AbstractMediaPlayer -> IjkMediaPlayer -> ijkplayer_jni.c -> ijkplayer.c -> Ff_ffplayer.c
时间: 2024-10-12 11:12:54

ExoPlayer备忘录的相关文章

Java设计模式应用——备忘录模式

备忘录模式主要用于存档.游戏中我们打boss前总会存档,如果打boss失败,则读取存档,重新挑战boss. 可以看出来,备忘录模式一般包括如下数据结构 1. 存档文件:用于恢复备份场景的必要数据: 2. 存档管理器:用于管理存档,包括存档的读写与展示: 3. 被存档的对象. 下面以射击游戏的存档来举例: 1. 射击选手 package com.coshaho.learn.memorandum; // 射击手 public class Shooter { // 血量 private int blo

Swift备忘录

Swift 备忘录 2015-4 一.简介 1.Swift 语言由苹果公司在2010年7月开始设计,在 2014 年6月推出,在 2015 年 12 月 3 日开源 2.特点(官方): (1)苹果宣称 Swift 的特点是:快速.现代.安全.互动,而且明显优于 Objective-C 语言 (2)可以使用现有的 Cocoa 和 Cocoa Touch 框架 (3)Swift 取消了 Objective-C 的指针及其他不安全访问的使用 (4)舍弃 Objective-C 早期应用 Smallta

JAVA设计模式(20):行为型-备忘录模式(Memento)

场景 录入大批人员资料.正在录入当前人资料时,发现上一个人录错了,此时需要恢复上一个人的资料,再进行修改. word文档编辑时,忽然电脑死机或断电,再打开时,可以看到word提示恢复到以前的文档. 管理系统中,公文撤回功能.公文发出去后,想撤回来. 核心 就是保存某个对象内部状态的拷贝,这样以后就可以将该对象恢复到原先的状态. 结构 源发器类Originator 备忘录类Memento 负责人类CateTaker 开发中常见的应用场景 棋类游戏中的,悔棋 普通软件中的,撤销操作 数据库软件中的,

Java设计模式(十) 备忘录模式 状态模式

(十九)备忘录模式 备忘录模式目的是保存一个对象的某个状态,在适当的时候恢复这个对象. class Memento{ private String value; public Memento(String value){ this.value = value; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } } class Storage

Linux中tomcat开机启动配置脚本【参考其他文章的总结备忘录】

参考文章http://blog.sina.com.cn/s/blog_a57562c80101ic47.html http://blog.csdn.net/cheng168520/article/details/4312828 http://blog.sina.com.cn/s/blog_7f395ece0100ti5y.html 以前在自己本机上安装过一个Linux,后台应为系统崩溃,以前配置的开机启动脚本.数据库主从双备份.负载均衡等都没了,所以现在在重新配置一次,赶紧做个笔记防止自己以后又

4 行为型模式之- 备忘录模式

备忘录模式介绍: 备忘录模式是一种行为模式,该模式用于保存对象当前的状态,并且在之后可以再次恢复到此状态,这有点像我们平时说的"后悔"药.备忘录模式实现的方式需要保证被保存的对象状态不能被对象从外界访问,目的是为了保护好被保存的这些对象状态的完整性以及内部实现不向外暴露 备忘录模式的定义: 在不破坏封闭的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样,以后就可以将对象恢复到原先的状态 对于备忘录模式来说,比较贴切的场景应该是游戏中的存档功能,该功能就是将游戏进度存储到

Ubuntu 14.04 安装配置备忘录

完全在 Linux 下工作,大概有3年时间了. 之前都是用 Windows, 而把 Linux 装在虚拟机里,现在反过来,把 Windows 装在了虚拟机里,只是因为偶尔还要用网银的缘故. 以我这几年的使用经验, 一句话: Linux 用过之后就回不去了. 以下记录我的 Ubuntu 14.04 Linux 安装配置, 算是备忘录. 需要说明的一点是: 我从来不觉得使用 Debian, CentOS, 或者 Arch, Gentoo 的人很牛, 只能说明你们不珍惜时间,就爱瞎折腾, 当然如果你是

设计模式03备忘录(java)

先贴代码有空来写内容. 备忘录1 1 //简单的备忘录,只可以记录上一次修改前的状态,实现撤回一次的操作. 2 class Student{ 3 private String name; 4 private String age; 5 private String gender; 6 //在Student类中直接设立一个Student实例,用于存储该类对象的上一个状态 7 private Student memento=null; 8 //构造函数就不啰嗦了 9 public Student()

备忘录模式

备忘录模式,望文生义就知道它是用来做备忘的,或者可以直接说是“备份”.当需要保存当前状态,以便在不久要恢复此状态时,就可以使用“备忘录模式”.将当前”状态“备份,是不是又new一个类,然后将每个字段方法copy过去就可以了呢?或者说使用我们之前clone方法做深复制浅复制呢?其实不然,在<大话设计模式>中,作者提到了原因,这样会暴露更多的细节给客户端,不符合我们面向对象的思想.什么是暴露更多的细节给客户端?我们来看下面一段代码. 1 package day_27_memento; 2 3 /*