VideoView
来源:互联网 发布:java业余班 编辑:程序博客网 时间:2024/05/01 12:25
基本使用
VideoView mVv = (VideoView) findViewById(R.id.vv);//添加播放控制条,还是自定义好点mVv.setMediaController(new MediaController(this));//设置视频源播放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信息处理 :经常会碰到视频编码格式不支持的情况,这里还是处理一下,若不想弹出提示框就返回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; } */
错误信息
QCMediaPlayer.java
//常见错误: "无法播放此视频" -我测试的是:红米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.f4vMediaPlayer-JNI: QCMediaPlayer mediaplayer NOT presentMediaPlayer: Unable to create media playerMediaPlayer: Couldn't open file on client side, trying server sideMediaPlayer: error (1, -2147483648)MediaPlayer: Error (1,-2147483648)
有人说 用下面的方式可以处理该异常,但我是使用系统封装好的控件,这个操作不到吧? 先记录下:
MediaPlayer player = MediaPlayer.create(this, Uri.parse(sound_file_path));MediaPlayer player = MediaPlayer.create(this, soundRedId, loop);
全屏播放 - 横竖屏切换
androidmanifest.xml
中依然还是定义竖屏,并定义一个切换横纵屏按钮btnChange
:<activity android:name="lynxz.org.video.VideoActivity" android:configChanges="keyboard|orientation|screenSize" android:screenOrientation="portrait" android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
布局:需要在
VidioView
外层套一个容器,比如:<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>
这么做是为了在切换屏幕方向的时候对
rl_vv
进行拉伸,而内部的VideoView
会依据视频尺寸重新计算宽高,我们看看其onMeasure()
源码就明了了,但若是直接具体指定了view的宽高,则视频会被拉伸://VideoView.java @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = getDefaultSize(mVideoWidth, widthMeasureSpec); int height = getDefaultSize(mVideoHeight, heightMeasureSpec); ...... setMeasuredDimension(width, height);}
按钮监听,手动切换
btnSwitch.setOnClickListener(View -> { if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); }});
设置VideoView布局尺寸
@Overridepublic 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.javapublic 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中进行横竖屏切换,则需要在onCreateView中
setRetainInstance(true);
,这样旋转后,才不会重新创建从头开始播放;
获取第一帧的内容作为封面
参考文章
@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(Schedulers.io()) .subscribeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<Bitmap>() { @Override public void call(Bitmap bitmap) { //设置封面 mYourVideoPlayerContainer.setBackgroundDrawable(new BitmapDrawable(bitmap)); } });}
滑动改变屏幕亮度/音量
权限申请
<uses-permission android:name="android.permission.WRITE_SETTINGS"/><uses-permission android:name="android.permission.VIBRATE"/> //按需申请
修改亮度方法
/*设置当前屏幕亮度值 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); // 保存设置的屏幕亮度值 // Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, (int) value);}
设置屏幕亮度模式方法 (自动/手动)
// value 可取值: Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC / SCREEN_BRIGHTNESS_MODE_MANUALprivate void setScreenMode(int value) { Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE, value);}
监听播放区域
mGestureDetector = new GestureDetector(this, mGestureListener);vv.setOnTouchListener(this); @Overridepublic boolean onTouch(View v, MotionEvent event) { return mGestureDetector.onTouchEvent(event);}
onScroll的时候动态改变亮度
onDown()
/onScroll()
返回trueprivate 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()
方法,调用以下操作
private void setVoiceVolume(boolean volumeUp) { // 设置音量绝对值的话,我在小米上突破不了限制,最大音量15,但是设置到10的时候就没法再增加了,最后使用系统的音量控制才可以 // 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); //降低音量,调出系统音量控制 if (volumeUp) { mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE, AudioManager.FX_FOCUS_NAVIGATION_UP); } else {//增加音量,调出系统音量控制 mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_LOWER, AudioManager.FX_FOCUS_NAVIGATION_UP); } }
- 在页面关闭时可考虑恢复亮度/音量初始值
- 在onTouch的时候对触点进行判断,区分是修改音量或是改变亮度
需要处理的问题
拖动进度条,手动seekTo后,进度会跳动
断点跟踪后发现是native方法的问题,各大视频播放平台的客户端,比较普遍存在,暂无法处理:
找到些资源:
- 关于Android VideoView seekTo不准确的解决方案
- 视频关键帧提取
第一个提到的关键帧问题,我找了个视频测试了下,seekTo到固定的时间点,则跳变的位置也固定;
暂停/恢复 页面时,视频重新加载
现象: 在视频播放时,使页面 onPause()
,之后再恢复,则 videoView
会重新开始播放,临时的处理方案是在 onPause()
的时候记录当前播放进度位置,在 onResume()
的时候拖动到该进度位置,但是该方案仍会有黑屏现象,代码如下:
int mPlayingPos = 0;@Overrideprotected void onPause() { mPlayingPos = mVideoView.getCurrentPosition(); //先获取再stopPlay(),原因自己看源码 mVideoView.stopPlayback(); super.onPause();}@Overrideprotected void onResume() { if (mPlayingPos > 0) { //此处为更好的用户体验,可添加一个progressBar,有些客户端会在这个过程中隐藏底下控制栏,这方法也不错 mVideoView.start(); mVideoView.seekTo(mPlayingPos); mPlayingPos = 0; } super.onResume();}
找到些可能相关的文章,链接已失效,快照如下(还得去看看 surfaceView
啊 ~ ~# ):
另一篇类似的: android开发常见问题 问题7,也指明是 surfaceview
的原因,之所以是黑色的见后面的解释:
Activity 调用的顺序是 onPause() -> onStop()
SurfaceView 调用了 surfaceDestroyed() 方法
然后再切回程序
Activity 调用的顺序是 onRestart() -> onStart() -> onResume ()
SurfaceView` 调用了 surfaceChanged() -> surfaceCreated() 方法
按挂断键或锁定屏幕
Activity 只调用 onPause() 方法
解锁后 Activity 调用 onResume() 方法
SurfaceView 什么方法都不调用
网络变化/切换应用后恢复播放
播放过程中,假如只缓冲了一部分视频,则当播放完缓冲部分后,会抛出1004异常,即使此时网络连接已经恢复,控件也不会自动继续缓冲:MediaPlayer: error (1, -1004)
源码注释: File or network related operation errors
同时,由于SurfaceView在页面onStop()时会destroy,比如播放时,用户按下home键或切换到其他应用页面再返回时,视频播放停止,此时需要重新加载视频并播放到上次停止的位置;
另外,有测试发现在三星G9200手机上,报 1004
这个错的时候会弹出错误提示框,然后卡死重启...
对比了下日志:
// API23 MediaPlayer.java@Overridepublic 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.javapublic 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) { //如果这里没有处理,则每次发生异常都会弹出提示框,可能造成崩溃 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; }};
因此需要对网络变化进行监听:
<uses-permission android:name="android.permission.INTERNET"/><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
@Overrideprotected void onResume() { super.onResume(); mNetworkState = NetworkHelper.getNetworkType(this); //播放网络视频时,需要检测判断网络状态变化 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; } }}/** * 监听网络变化,用于重新缓冲 */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));}/** * 网络播放 */public void doWhenNetworkChange() { mNetworkState = NetworkHelper.getNetworkType(this); //保存当前已缓存长度 int bufferPercentage = mVv.getBufferPercentage(); mLastLoadLength = bufferPercentage * mVv.getDuration() / 100; //这里需要判断 0 int currentPosition = mVv.getCurrentPosition(); if (currentPosition > 0) { mPlayingPos = currentPosition; } debugLog(bufferPercentage + " 网络变化 ... " + mNetworkState + " 缓存长度 " + mLastLoadLength + " -- " + currentPosition); if (mNetworkState == NetworkHelper.NETWORK_TYPE_INVALID && bufferPercentage < 100) { // 监听当前播放位置,在达到缓冲长度前自动停止 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 体验好点 if (mCheckPlayingProgressTimer != null) { mCheckPlayingProgressTimer.cancel(); mCheckPlayingProgressTimer = null; } mVv.setVideoURI(mVideoUri); mVv.start(); mVv.seekTo(mPlayingPos); mLastLoadLength = -1; mPlayingPos = 0;}@Overrideprotected void onPause() { mPlayingPos = mVv.getCurrentPosition(); mVv.pause(); super.onPause();}@Overrideprotected void onStop() { mVv.stopPlayback(); mLastLoadLength = 0; debugLog("onResume " + mPlayingPos + " -- " + mLastLoadLength); super.onStop();}@Overrideprotected void onDestroy() { super.onDestroy(); if (mCheckPlayingProgressTimer != null) { mCheckPlayingProgressTimer.cancel(); mCheckPlayingProgressTimer = null; } ...... unregisterNetworkReceiver();}
seekbar变化超出缓冲长度
使用系统提供的控件 mVv.setMediaController(new MediaController(this));
的话,在断网时,仍可以拖动超出缓冲长度的范围,会报错,这个还是得自定义才能控制可拖动位置,不再赘述;
MediaPlayer: Attempt to perform seekTo in wrong state: mPlayer=0x7f7ebbf5c0, mCurrentState=0MediaPlayer: Error (1,-1004)
//API 23 MediaController.javaprivate 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; } //这里就只是设定mediaplayer的播放位置而已 long duration = mPlayer.getDuration(); long newposition = (duration * progress) / 1000L; mPlayer.seekTo( (int) newposition); if (mCurrentTime != null) mCurrentTime.setText(stringForTime( (int) newposition)); }}
当断网后,用户拖动超出缓冲区长度的话mediaplayer报错,此时再次点击VideoView区域,不会触发显示控制条,真是各种不方便啊,还是建议自己写一个控制条;
SurfaceView
资源
- SurfaceView 源码分析及使用
这篇讲到了SurfaceView
会显示黑色区域的原因:SurfaceView 的 draw 和 dispatchDraw 方法中看到,SurfaceView 中,windownType变量被初始化为WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA,所以在创建绘制这个 View 的过程中整个 Canvas 会被涂成黑色
- 浮层视频效果,在另外一个Window使用SurfaceView无法正常显示的问题排查与解决
surfaceView黑屏
无内容时,默认会绘制黑色背景图
//SurfaceView.javaint 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);}
感谢@尚弟很忙哒 的提醒, 设置页面主题为透明(
android:theme="@android:style/Theme.Translucent"
)时,在初始缓冲阶段,VideoView区域会变成透明:<?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类开头有酱紫的一段注释,大概能解释为什么缓冲时候视频区域会透明了:
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);
不过挪动之后就可以设置VideoView的背景,此时才不会遮盖实际的视频绘图了,xml中指定吧,这里省略,不过如果VideoView区域还有其他控件的话,会被遮盖,所以最后我就没设定zorderOnTop了,而是直接在xml中指定VideoView的背景色,然后在onPrepare回调的时候,去掉背景即可(按需延时,或者在有播放进度,要更新进度条的时候进行去掉背景操作都ok,不然可能会有一瞬间的透明):
mVv.setBackgroundColor(Color.TRANSPARENT);
之前是打算像网上说的给VideoView的holder添加一个callback,(
mVv.getHolder().addCallback(new SurfaceHolder.Callback() {...}
) ,在surfaceCreated()
的时候获取canvas并手动绘制背景色,但是holder.lockCanvas()
一直返回null
,log信息提示:E/SurfaceHolder: Exception locking surface java.lang.IllegalArgumentException at android.view.Surface.nativeLockCanvas(Native Method) .......
看到native我暂时就没招了,打住,老实用变通方法吧;
手机 "菜单键" 导致应用被stop,虽然此时看起来可见
SurfaceView.java
的注释: 在调用菜单键的时候虽然页面貌似可见,但实际已经调用了onStop()方法了,而surface在window不可见时会销毁: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支持及相关原理介绍
- Stagefright功能扩展 这篇论文前半部分有关于多媒体框架调用的介绍
- VideoView
- videoView
- VideoView
- VideoView
- VideoView
- Android-- VideoView
- VideoView笔记
- MediaController+VideoView
- 11 VideoView
- videoView用法
- Videoview 在线播放
- VideoView小试牛刀
- VideoView 官方
- android videoview
- VideoView 黑屏
- VideoView+SeekBar
- Android VideoView播放视频
- android videoview 背景知识
- WINDOWS下批处理脚本命令
- LeetCode-120. Triangle
- neo4j-ogm-core使用小记
- JS的节点操作:创建、增加、删除、复制、查找
- 002一番寻找后,终于找到简单好用的利用runtime解决数组字典为nil时造成的程序崩溃的第三方
- VideoView
- 一个对含中文字符串在内的字符串排序简便方法
- LeetCode 338
- linux系统的简述和优势,趋势所在!!
- BZOJ1880: [Sdoi2009]Elaxia的路线
- ROS里面的几个python写的工具
- 你中招了吗?加密勒索软件攻击趋势分析
- BZOJ1133: [POI2009]Kon
- strcpy_s,sprintf_s,wcscpy_s,swprintf_s,wcscat_s,加了_s就真的安全吗?