項(xiàng)目需要做一個(gè)簡單的播放視頻功能demo,后期會換成公司自己的組件,所以就沒考慮使用第三方庫了,直接上系統(tǒng)的VideoView,在這里記錄下操作;
順便吐槽下:一直都聽說簡書編輯器好用,第一次使用,有點(diǎn)失望,markdown跟效果分欄竟然不能同步滾動,也不支持[TOC],沒有個(gè)目錄實(shí)在很不習(xí)慣,表情圖標(biāo)也不能插入,代碼區(qū)塊加空行經(jīng)常都識別不了啊,就不能讓我簡單滴從筆記中直接粘貼md文本嗎... ==!
Demo項(xiàng)目下載
自己封裝了一個(gè)播放器
VideoView
資源
- Android三種播放視頻的方式
- Android播放器框架分析之AwesomePlayer
- 音頻與視頻播放 講的player類,比較全
- Android視頻播放器實(shí)現(xiàn)小窗口和全屏狀態(tài)切換
視頻播放原理:
系統(tǒng)會首先確定視頻的格式,然后得到視頻的編碼..然后對編碼進(jìn)行解碼,得到一幀一幀的圖像,最后在畫布上進(jìn)行迅速更新,顯然需要在獨(dú)立的線程中完成,這時(shí)就需要使用surfaceView了

基本使用
VideoView mVv = (VideoView) findViewById(R.id.vv);
//添加播放控制條,還是自定義好點(diǎn)
mVv.setMediaController(new MediaController(this));
//設(shè)置視頻源播放res/raw中的文件,文件名小寫字母,格式: 3gp,mp4等,flv的不一定支持;
Uri rawUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.shuai_dan_ge);
mVv.setVideoURI(rawUri);
// 播放在線視頻
mVideoUri = Uri.parse("http://****/abc.mp4");
mVv.setVideoPath(mVideoUri.toString());
mVv.start();
mVv.requestFocus();
/*
其他方法:
mVv.pause();
mVv.stop();
mVv.resume();
mVv.setOnPreparedListener(this);
mVv.setOnErrorListener(this);
mVv.setOnCompletionListener(this);**
Error信息處理 :
經(jīng)常會碰到視頻編碼格式不支持的情況,這里還是處理一下,若不想彈出提示框就返回true;
http://developer.android.com/intl/zh-cn/reference/android/media/MediaPlayer.OnErrorListener.html
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
if(what==MediaPlayer.MEDIA_ERROR_SERVER_DIED){
Log.v(TAG,"Media Error,Server Died"+extra);
}else if(what==MediaPlayer.MEDIA_ERROR_UNKNOWN){
Log.v(TAG,"Media Error,Error Unknown "+extra);
}
return true;
}
*/
錯誤信息
//常見錯誤: "無法播放此視頻" -我測試的是:紅米1s電信版4.4.4無法播放,但在三星s6(5.1.1)上就可以播放
//播放源:http://27.152.191.198/c12.e.99.com/b/p/67/c4ff9f6535ac41a598bb05bf5b05b185/c4ff9f6535ac41a598bb05bf5b05b185.v.854.480.f4v
MediaPlayer-JNI: QCMediaPlayer mediaplayer NOT present
MediaPlayer: Unable to create media player
MediaPlayer: Couldn't open file on client side, trying server side
MediaPlayer: error (1, -2147483648)
MediaPlayer: Error (1,-2147483648)
有人說 用下面的方式可以處理該異常,但我是使用系統(tǒng)封裝好的控件,這個(gè)操作不到吧? 先記錄下:
MediaPlayer player = MediaPlayer.create(this, Uri.parse(sound_file_path));
MediaPlayer player = MediaPlayer.create(this, soundRedId, loop);
全屏播放 - 橫豎屏切換
-
androidmanifest.xml中依然還是定義豎屏,并定義一個(gè)切換橫縱屏按鈕btnChange:
<activity
android:name="lynxz.org.video.VideoActivity"
android:configChanges="keyboard|orientation|screenSize"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
- 布局:需要在
VidioView外層套一個(gè)容器,比如:
<RelativeLayout
android:id="@+id/rl_vv"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@android:color/black"
android:minHeight="200dp"
android:visibility="visible">
<VideoView
android:id="@+id/vv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
</RelativeLayout>
這么做是為了在切換屏幕方向的時(shí)候?qū)?rl_vv 進(jìn)行拉伸,而內(nèi)部的 VideoView 會依據(jù)視頻尺寸重新計(jì)算寬高,我們看看其 onMeasure() 源碼就明了了,但若是直接具體指定了view的寬高,則視頻會被拉伸:
//VideoView.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
......
setMeasuredDimension(width, height);
}
- 按鈕監(jiān)聽,手動切換
btnSwitch.setOnClickListener(View -> {
if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
});
設(shè)置VideoView布局尺寸
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (mVv == null) {
return;
}
if (this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE){//橫屏
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().getDecorView().invalidate();
float height = DensityUtil.getWidthInPx(this);
float width = DensityUtil.getHeightInPx(this);
mRlVv.getLayoutParams().height = (int) width;
mRlVv.getLayoutParams().width = (int) height;
} else {
final WindowManager.LayoutParams attrs = getWindow().getAttributes();
attrs.flags &= (~WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().setAttributes(attrs);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
float width = DensityUtil.getWidthInPx(this);
float height = DensityUtil.dip2px(this, 200.f);
mRlVv.getLayoutParams().height = (int) height;
mRlVv.getLayoutParams().width = (int) width;
}
}
自定義工具類
//DensityUtil.java
public static final float getHeightInPx(Context context) {
final float height = context.getResources().getDisplayMetrics().heightPixels;
return height;
}
public static final float getWidthInPx(Context context) {
final float width = context.getResources().getDisplayMetrics().widthPixels;
return width;
}
另外,如果是將播放器放于fragment中進(jìn)行橫豎屏切換,則需要在onCreateView中setRetainInstance(true);,這樣旋轉(zhuǎn)后,才不會重新創(chuàng)建從頭開始播放;
獲取第一幀的內(nèi)容作為封面
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
private void createVideoThumbnail() {
Observable<Bitmap> observable = Observable.create(new Observable.OnSubscribe<Bitmap>() {
@Override
public void call(Subscriber<? super Bitmap> subscriber) {
Bitmap bitmap = null;
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
int kind = MediaStore.Video.Thumbnails.MINI_KIND;
if (Build.VERSION.SDK_INT >= 14) {
retriever.setDataSource(mVideoUrl, new HashMap<String, String>());
} else {
retriever.setDataSource(mVideoUrl);
}
bitmap = retriever.getFrameAtTime();
subscriber.onNext(bitmap);
retriever.release();
}
});
observable.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new Action1<Bitmap>() {
@Override
public void call(Bitmap bitmap) {
//設(shè)置封面
mYourVideoPlayerContainer.setBackgroundDrawable(new BitmapDrawable(bitmap));
}
});
}
滑動改變屏幕亮度/音量
- 權(quán)限申請
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<uses-permission android:name="android.permission.VIBRATE"/> //按需申請
- 修改亮度方法
/*設(shè)置當(dāng)前屏幕亮度值 0--255,并使之生效*/
private void setScreenBrightness(float value) {
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.screenBrightness = lp.screenBrightness + value / 255.0f;
Vibrator vibrator;
if (lp.screenBrightness > 1) {
lp.screenBrightness = 1;
// vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
// long[] pattern = {10, 200}; // OFF/ON/OFF/ON...
// vibrator.vibrate(pattern, -1);
} else if (lp.screenBrightness < 0.2) {
lp.screenBrightness = (float) 0.2;
// vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
// long[] pattern = {10, 200}; // OFF/ON/OFF/ON...
// vibrator.vibrate(pattern, -1);
}
getWindow().setAttributes(lp);
// 保存設(shè)置的屏幕亮度值
// Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, (int) value);
}
- 設(shè)置屏幕亮度模式方法 (自動/手動)
// value 可取值: Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC / SCREEN_BRIGHTNESS_MODE_MANUAL
private void setScreenMode(int value) {
Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE, value);
}
- 監(jiān)聽播放區(qū)域
mGestureDetector = new GestureDetector(this, mGestureListener);
vv.setOnTouchListener(this);
@Override
public boolean onTouch(View v, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
- onScroll的時(shí)候動態(tài)改變亮度
onDown()/onScroll()返回true
private android.view.GestureDetector.OnGestureListener mGestureListener = new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
final double FLING_MIN_VELOCITY = 0.5;
final double FLING_MIN_DISTANCE = 0.5;
if (e1.getY() - e2.getY() > FLING_MIN_DISTANCE
&& Math.abs(distanceY) > FLING_MIN_VELOCITY) {
setScreenBrightness(20);
}
if (e1.getY() - e2.getY() < FLING_MIN_DISTANCE
&& Math.abs(distanceY) > FLING_MIN_VELOCITY) {
setScreenBrightness(-20);
}
return true;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return true;
}
};
滑動修改音量
修改上方的 onScroll() 方法,調(diào)用以下操作
private void setVoiceVolume(boolean volumeUp) {
// 設(shè)置音量絕對值的話,我在小米上突破不了限制,最大音量15,但是設(shè)置到10的時(shí)候就沒法再增加了,最后使用系統(tǒng)的音量控制才可以
// int currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
// int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
// int flag = volumeUp ? 1 : -1;
// currentVolume += flag * 1;
// if (currentVolume >= maxVolume) {
// currentVolume = maxVolume;
// } else if (currentVolume <= 1) {
// currentVolume = 1;
// }
// Log.i(TAG, "setVoiceVolume currentVolume = " + currentVolume + " ,maxVolume = " + maxVolume);
// mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume, 0);
//降低音量,調(diào)出系統(tǒng)音量控制
if (volumeUp) {
mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE,
AudioManager.FX_FOCUS_NAVIGATION_UP);
} else {//增加音量,調(diào)出系統(tǒng)音量控制
mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_LOWER,
AudioManager.FX_FOCUS_NAVIGATION_UP);
}
}
- 在頁面關(guān)閉時(shí)可考慮恢復(fù)亮度/音量初始值
- 在onTouch的時(shí)候?qū)τ|點(diǎn)進(jìn)行判斷,區(qū)分是修改音量或是改變亮度
需要處理的問題
拖動進(jìn)度條,手動seekTo后,進(jìn)度會跳動
斷點(diǎn)跟蹤后發(fā)現(xiàn)是native方法的問題,各大視頻播放平臺的客戶端,比較普遍存在,暫無法處理:

找到些資源:
- 關(guān)于Android VideoView seekTo不準(zhǔn)確的解決方案
-
視頻關(guān)鍵幀提取
第一個(gè)提到的關(guān)鍵幀問題,我找了個(gè)視頻測試了下,seekTo到固定的時(shí)間點(diǎn),則跳變的位置也固定;
暫停/恢復(fù) 頁面時(shí),視頻重新加載
現(xiàn)象: 在視頻播放時(shí),使頁面 onPause() ,之后再恢復(fù),則 videoView 會重新開始播放,臨時(shí)的處理方案是在 onPause() 的時(shí)候記錄當(dāng)前播放進(jìn)度位置,在 onResume() 的時(shí)候拖動到該進(jìn)度位置,但是該方案仍會有黑屏現(xiàn)象,代碼如下:
int mPlayingPos = 0;
@Override
protected void onPause() {
mPlayingPos = mVideoView.getCurrentPosition(); //先獲取再stopPlay(),原因自己看源碼
mVideoView.stopPlayback();
super.onPause();
}
@Override
protected void onResume() {
if (mPlayingPos > 0) {
//此處為更好的用戶體驗(yàn),可添加一個(gè)progressBar,有些客戶端會在這個(gè)過程中隱藏底下控制欄,這方法也不錯
mVideoView.start();
mVideoView.seekTo(mPlayingPos);
mPlayingPos = 0;
}
super.onResume();
}
找到些可能相關(guān)的文章,鏈接已失效,快照如下(還得去看看 surfaceView 啊 ~ ~# ):
另一篇類似的: android開發(fā)常見問題 問題7,也指明是 surfaceview 的原因,之所以是黑色的見后面的解釋:
Activity 調(diào)用的順序是 onPause() -> onStop()
SurfaceView 調(diào)用了 surfaceDestroyed() 方法
然后再切回程序
Activity 調(diào)用的順序是 onRestart() -> onStart() -> onResume ()
SurfaceView` 調(diào)用了 surfaceChanged() -> surfaceCreated() 方法
按掛斷鍵或鎖定屏幕
Activity 只調(diào)用 onPause() 方法
解鎖后 Activity 調(diào)用 onResume() 方法
SurfaceView 什么方法都不調(diào)用
網(wǎng)絡(luò)變化/切換應(yīng)用后恢復(fù)播放
播放過程中,假如只緩沖了一部分視頻,則當(dāng)播放完緩沖部分后,會拋出1004異常,即使此時(shí)網(wǎng)絡(luò)連接已經(jīng)恢復(fù),控件也不會自動繼續(xù)緩沖:
MediaPlayer: error (1, -1004)
源碼注釋: File or network related operation errors
同時(shí),由于SurfaceView在頁面onStop()時(shí)會destroy,比如播放時(shí),用戶按下home鍵或切換到其他應(yīng)用頁面再返回時(shí),視頻播放停止,此時(shí)需要重新加載視頻并播放到上次停止的位置;
另外,有測試發(fā)現(xiàn)在三星G9200手機(jī)上,報(bào) 1004 這個(gè)錯的時(shí)候會彈出錯誤提示框,然后卡死重啟...
對比了下日志:
// API23 MediaPlayer.java
@Override
public void handleMessage(Message msg) {
if (mMediaPlayer.mNativeContext == 0) {
Log.w(TAG, "mediaplayer went away with unhandled events");
return;
}
switch(msg.what) {
case MEDIA_ERROR:
Log.e(TAG, "Error (" + msg.arg1 + "," + msg.arg2 + ")");
boolean error_was_handled = false;
if (mOnErrorListener != null) {
error_was_handled = mOnErrorListener.onError(mMediaPlayer, msg.arg1, msg.arg2);
}
if (mOnCompletionListener != null && ! error_was_handled) {
mOnCompletionListener.onCompletion(mMediaPlayer);
}
stayAwake(false);
return;
default:
Log.e(TAG, "Unknown message type " + msg.what);
return;
}
}
//API23 VideoView.java
public void setOnErrorListener(OnErrorListener l){
mOnErrorListener = l;
}
private MediaPlayer.OnErrorListener mErrorListener =
new MediaPlayer.OnErrorListener() {
public boolean onError(MediaPlayer mp, int framework_err, int impl_err) {
......
/* If an error handler has been supplied, use it and finish. */
if (mOnErrorListener != null) { //如果這里沒有處理,則每次發(fā)生異常都會彈出提示框,可能造成崩潰
if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) {
return true;
}
}
/* Otherwise, pop up an error dialog so the user knows that
* something bad has happened. Only try and pop up the dialog
* if we're attached to a window. When we're going away and no
* longer have a window, don't bother showing the user an error.
*/
if (getWindowToken() != null) {
Resources r = mContext.getResources();
int messageId;
if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback;
} else {
messageId = com.android.internal.R.string.VideoView_error_text_unknown;
}
// 彈出錯誤提示框
new AlertDialog.Builder(mContext)
.setMessage(messageId)
.setPositiveButton(com.android.internal.R.string.VideoView_error_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
/* If we get here, there is no onError listener, so
* at least inform them that the video is over.
*/
if (mOnCompletionListener != null) {
mOnCompletionListener.onCompletion(mMediaPlayer);
}
}
})
.setCancelable(false)
.show();
}
return true;
}
};
因此需要對網(wǎng)絡(luò)變化進(jìn)行監(jiān)聽:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
@Override
protected void onResume() {
super.onResume();
mNetworkState = NetworkHelper.getNetworkType(this);
//播放網(wǎng)絡(luò)視頻時(shí),需要檢測判斷網(wǎng)絡(luò)狀態(tài)變化
if (SCHEME_HTTP.equalsIgnoreCase(mVideoUri.getScheme()) && mNetworkState == 0) {
MessageUtils.showAlertDialog(this, "提示", getResources().getString(R.string.network_error), null);
} else {
if (mPlayingPos > 0) {
mVv.start();
mVv.seekTo(mPlayingPos);
mPlayingPos = 0;
}
}
}
/**
* 監(jiān)聽網(wǎng)絡(luò)變化,用于重新緩沖
*/
private void registerNetworkReceiver() {
if (mNetworkReceiver == null) {
mNetworkReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (SCHEME_HTTP.equalsIgnoreCase(mVideoUri.getScheme())
&& action.equalsIgnoreCase(ConnectivityManager.CONNECTIVITY_ACTION)) {
doWhenNetworkChange();
}
}
};
}
registerReceiver(mNetworkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
/**
* 網(wǎng)絡(luò)播放
*/
public void doWhenNetworkChange() {
mNetworkState = NetworkHelper.getNetworkType(this);
//保存當(dāng)前已緩存長度
int bufferPercentage = mVv.getBufferPercentage();
mLastLoadLength = bufferPercentage * mVv.getDuration() / 100;
//這里需要判斷 0
int currentPosition = mVv.getCurrentPosition();
if (currentPosition > 0) {
mPlayingPos = currentPosition;
}
debugLog(bufferPercentage + " 網(wǎng)絡(luò)變化 ... " + mNetworkState + " 緩存長度 " + mLastLoadLength + " -- " + currentPosition);
if (mNetworkState == NetworkHelper.NETWORK_TYPE_INVALID && bufferPercentage < 100) {
// 監(jiān)聽當(dāng)前播放位置,在達(dá)到緩沖長度前自動停止
if (mCheckPlayingProgressTimer == null) {
mCheckPlayingProgressTimer = new Timer();
}
mCheckPlayingProgressTimer.schedule(new TimerTask() {
@Override
public void run() {
if (mPlayingPos >= mLastLoadLength - deltaTime) {
mVv.pause();
}
}
}, 0, 1000);//每秒檢測一次
} else {
restartPlayVideo();
}
}
private void restartPlayVideo() {
//todo 添加 progressBar 體驗(yàn)好點(diǎn)
if (mCheckPlayingProgressTimer != null) {
mCheckPlayingProgressTimer.cancel();
mCheckPlayingProgressTimer = null;
}
mVv.setVideoURI(mVideoUri);
mVv.start();
mVv.seekTo(mPlayingPos);
mLastLoadLength = -1;
mPlayingPos = 0;
}
@Override
protected void onPause() {
mPlayingPos = mVv.getCurrentPosition();
mVv.pause();
super.onPause();
}
@Override
protected void onStop() {
mVv.stopPlayback();
mLastLoadLength = 0;
debugLog("onResume " + mPlayingPos + " -- " + mLastLoadLength);
super.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mCheckPlayingProgressTimer != null) {
mCheckPlayingProgressTimer.cancel();
mCheckPlayingProgressTimer = null;
}
......
unregisterNetworkReceiver();
}
seekbar變化超出緩沖長度
使用系統(tǒng)提供的控件 mVv.setMediaController(new MediaController(this)); 的話,在斷網(wǎng)時(shí),仍可以拖動超出緩沖長度的范圍,會報(bào)錯,這個(gè)還是得自定義才能控制可拖動位置,不再贅述;
MediaPlayer: Attempt to perform seekTo in wrong state: mPlayer=0x7f7ebbf5c0, mCurrentState=0
MediaPlayer: Error (1,-1004)
//API 23 MediaController.java
private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
if (!fromuser) {
// We're not interested in programmatically generated changes to
// the progress bar's position.
return;
}
//這里就只是設(shè)定mediaplayer的播放位置而已
long duration = mPlayer.getDuration();
long newposition = (duration * progress) / 1000L;
mPlayer.seekTo( (int) newposition);
if (mCurrentTime != null)
mCurrentTime.setText(stringForTime( (int) newposition));
}
}
當(dāng)斷網(wǎng)后,用戶拖動超出緩沖區(qū)長度的話mediaplayer報(bào)錯,此時(shí)再次點(diǎn)擊VideoView區(qū)域,不會觸發(fā)顯示控制條,真是各種不方便啊,還是建議自己寫一個(gè)控制條;
SurfaceView
資源
-
SurfaceView 源碼分析及使用
這篇講到了SurfaceView會顯示黑色區(qū)域的原因:
SurfaceView 的 draw 和 dispatchDraw 方法中看到,SurfaceView 中,windownType變量被初始化為WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA,所以在創(chuàng)建繪制這個(gè) View 的過程中整個(gè) Canvas 會被涂成黑色
surfaceView黑屏
- 無內(nèi)容時(shí),默認(rèn)會繪制黑色背景圖
//SurfaceView.java
int mWindowType = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
public void draw(Canvas canvas) {
if (mWindowType != WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
// draw() is not called when SKIP_DRAW is set
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
// punch a whole in the view-hierarchy below us
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
}
super.draw(canvas);
}
感謝@尚弟很忙噠 的提醒, 設(shè)置頁面主題為透明(android:theme="@android:style/Theme.Translucent")時(shí),在初始緩沖階段,VideoView區(qū)域會變成透明:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="org.lynxz.androiddemos.VideoViewActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@android:color/holo_blue_bright">
<VideoView
android:id="@+id/vv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
</RelativeLayout>
</RelativeLayout>
在SurfaceView類開頭有醬紫的一段注釋,大概能解釋為什么緩沖時(shí)候視頻區(qū)域會透明了:
The surface is Z ordered so that it is behind the window holding its SurfaceView;
the SurfaceView punches a hole in its window to allow its surface to be displayed.
因此處理方案就變成將SurfaceView挪到上層即可:
mVv.setZOrderOnTop(true);
不過挪動之后就可以設(shè)置VideoView的背景,此時(shí)才不會遮蓋實(shí)際的視頻繪圖了,xml中指定吧,這里省略,不過如果VideoView區(qū)域還有其他控件的話,會被遮蓋,所以最后我就沒設(shè)定zorderOnTop了,而是直接在xml中指定VideoView的背景色,然后在onPrepare回調(diào)的時(shí)候,去掉背景即可(按需延時(shí),或者在有播放進(jìn)度,要更新進(jìn)度條的時(shí)候進(jìn)行去掉背景操作都o(jì)k,不然可能會有一瞬間的透明):
mVv.setBackgroundColor(Color.TRANSPARENT);
之前是打算像網(wǎng)上說的給VideoView的holder添加一個(gè)callback,(mVv.getHolder().addCallback(new SurfaceHolder.Callback() {...}) ,在 surfaceCreated() 的時(shí)候獲取canvas并手動繪制背景色,但是 holder.lockCanvas() 一直返回 null ,log信息提示:
E/SurfaceHolder: Exception locking surface
java.lang.IllegalArgumentException
at android.view.Surface.nativeLockCanvas(Native Method)
.......
看到native我暫時(shí)就沒招了,打住,老實(shí)用變通方法吧;
- 手機(jī) "菜單鍵" 導(dǎo)致應(yīng)用被stop,雖然此時(shí)看起來可見
SurfaceView.java的注釋: 在調(diào)用菜單鍵的時(shí)候雖然頁面貌似可見,但實(shí)際已經(jīng)調(diào)用了onStop()方法了,而surface在window不可見時(shí)會銷毀:
The Surface will be created for you while the SurfaceView's window is
visible; you should implement {@link SurfaceHolder.Callback#surfaceCreated}
and {@link SurfaceHolder.Callback#surfaceDestroyed} to discover when the
Surface is created and destroyed as the window is shown and hidden.

- VideoView無法播放f4v格式(三星s6可以播放,紅米1s(4.4.4)播放失敗)....
以后能力夠了可以參考下這篇 :- Android平臺Stagefright中增加flv/f4v支持及相關(guān)原理介紹
- Stagefright功能擴(kuò)展 這篇論文前半部分有關(guān)于多媒體框架調(diào)用的介紹