編輯:關於Android編程
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. 支持自定義和拓展使用場景。
(本節說明重點為demo。)
簡單來說,上層調用方式基本為:
PlayerActivity -> DemoPlayer -> ExoPlayer
PlayerActivity -> RendererBuilder -> ExtractorRendererBuilder
類圖為:
vcPmv9jWxsHLsqW3xcb3RGVtb1BsYXllcqOs0ru3vcPm0aHU8cHLUmVuZGVyZXKhozwvcD4NCjxwPtXiwO+1xFJlbmRlcmVy1ri2qMHLyv2+3dS0uPHKvaGiveLC67e9yr26zbu6s+XH+LTz0KG1yKGjo6jLtcP3o6zV4sDvtcS7urPlx/i089Ch1rhSb2xsaW5nU2FtcGxlQnVmZmVytcS089Cho6yyu7vh07DP7L34yOuypbfFtcTL2bbIo6zWu7vh07DP7Lu6tObK/b7dtcTX7rTz1rWjqTwvcD4NCjxwPkV4b1BsYXllctTyysfDvczlQVBJvdO/2qGjPC9wPg0KPHA+RGVtb1BsYXllctbQ1rG907fi17DBy0V4b1BsYXllcrrNz+C52LvYtfe907/ao6y4utTwsqW3xcb3tcTC37ytv9jWxrrNtKvI61N1cmZhY2VWaWV3tciy2df3o6y2+LfHsqW3xcb3tcTE2rK/1K3A7aGjPC9wPg0KPHA+1eLA782ouf3KsdDyzbzAtMu1w/dEZW1v1tC8uLj2wOC1xLX308O6zbfi17C3vcq9oaM8L3A+DQo8cD48aW1nIGFsdD0="(demo時序圖)" src="/uploadfile/Collfiles/20160505/20160505090820572.jpg" title="\" />
簡單來說,代碼結構是這樣:
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中。
最後的過程有些復雜,流程圖如下所示:
通過以下這段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都在這裡注冊或注銷監聽。
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獲取錯誤的話,那麼會直接影響到播放流程中從緩沖區獲取數據和解碼器渲染數據等功能。
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.其他
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
為什麼要使用多線程下載呢?究其原因就一個字:"快",使用多線程下載的速度遠比單線程的下載速度要快,說到下載速度,決定下載速度的因素一般有兩個:一個是客
MavLink是輕量級的通訊協議,主要應用於終端與小型無人載具間的通訊。由於它的通用性,MavLink可以被翻譯成各種語言的代碼應用於各種不同的環境。具體如何通過工具來生
Android Overview Screen – 概覽界面原文鏈接:http://developer.android.com/guide/component
前言:自己在學習的過程中的一些操作過程,對分享的一些理解。下面就講解一下: 首先,我們需要去ShareSdk官方網站http://shares