Android开发之视频录制与播放
2017-09-29 10:32
627 查看
前言
公司产品有很多地方都需要上传音频视频,今天抽空总结一下音频视频的录制。学习的主角是MediaRecorder类。MediaRecorder类介绍:
MediaRecorder类是Android sdk提供的一个专门用于音视频录制,一般利用手机麦克风采集音频,摄像头采集图片信息。MediaRecorder主要函数:
setAudioChannels(int numChannels) 设置录制的音频通道数setAudioEncoder(int audio_encoder) 设置audio的编码格式
setAudioEncodingBitRate(int bitRate) 设置录制的音频编码比特率
setAudioSamplingRate(int samplingRate) 设置录制的音频采样率
setAudioSource(int audio_source) 设置用于录制的音源
setAuxiliaryOutputFile(String path) 辅助时间的推移视频文件的路径传递
setAuxiliaryOutputFile(FileDescriptor fd)在文件描述符传递的辅助时间的推移视频
setCamera(Camera c) 设置一个recording的摄像头
setCaptureRate(double fps) 设置视频帧的捕获率
setMaxDuration(int max_duration_ms) 设置记录会话的最大持续时间(毫秒)
setMaxFileSize(long max_filesize_bytes) 设置记录会话的最大大小(以字节为单位)
setOutputFile(FileDescriptor fd) 传递要写入的文件的文件描述符
setOutputFile(String path) 设置输出文件的路径
setOutputFormat(int output_format) 设置在录制过程中产生的输出文件的格式
setPreviewDisplay(Surface sv) 表面设置显示记录媒体(视频)的预览
setVideoEncoder(int video_encoder) 设置视频编码器,用于录制
setVideoEncodingBitRate(int bitRate) 设置录制的视频编码比特率
setVideoFrameRate(int rate) 设置要捕获的视频帧速率
setVideoSize(int width, int height) 设置要捕获的视频的宽度和高度
setVideoSource(int video_source) 开始捕捉和编码数据到setOutputFile(指定的文件)
setLocation(float latitude, float longitude) 设置并存储在输出文件中的地理数据(经度和纬度)
setProfile(CamcorderProfile profile) 指定CamcorderProfile对象
setOrientationHint(int degrees)设置输出的视频播放的方向提示
setOnErrorListener(MediaRecorder.OnErrorListener l)注册一个用于记录录制时出现的错误的监听器
setOnInfoListener(MediaRecorder.OnInfoListener listener)注册一个用于记录录制时出现的信息事件
getMaxAmplitude() 获取在前一次调用此方法之后录音中出现的最大振幅
prepare()准备录制。
release()释放资源
reset()将MediaRecorder设为空闲状态
start()开始录制
stop()停止录制
MediaRecorder主要配置参数:
1.)视频编码格式MediaRecorder.VideoEncoderdefault,H263,H264,MPEG_4_SP,VP8
2.)音频编码格式MediaRecorder.AudioEncoder
default,AAC,HE_AAC,AAC_ELD,AMR_NB,AMR_WB,VORBIS
3.)视频资源获取方式MediaRecorder.VideoSource
default,CAMERA,SURFACE
4.)音频资源获取方式MediaRecorder.AudioSource
defalut,camcorder,mic,voice_call,voice_communication,voice_downlink,voice_recognition, voice_uplink
5.)资源输出格式MediaRecorder.OutputFormat
amr_nb,amr_wb,default,mpeg_4,raw_amr,three_gpp,aac_adif, aac_adts, output_format_rtp_avp, output_format_mpeg2ts ,webm
MediaRecorder录制视频简单实现:
1).在清单文件中添加权限<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
2).在 app 文件夹中 build.gradle 添加
compile 'com.github.Othershe:NiceDialog:1.1.1' compile 'cn.jzvd:jiaozivideoplayer:6.0.1'
3).布局文件
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:background="#00ff00" android:orientation="vertical" android:gravity="center" tools:context="com.gyq.recordvideotest.MainActivity"> <LinearLayout android:id="@+id/ll_click_record" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/icon_video_nor"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#ffffff" android:layout_marginTop="12dp" android:text="点击录制视频" android:textSize="16sp"/> </LinearLayout> <Button android:onClick="play" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="play"/> </LinearLayout>
dialog 显示的布局
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <RelativeLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <SurfaceView android:id="@+id/surface_view" android:layout_width="match_parent" android:layout_height="match_parent"/> <Chronometer android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/timer" android:textColor="#ff0303" android:textSize="16sp" android:focusable="true"/> <ImageButton android:id="@+id/ib_play" android:layout_width="80dp" android:layout_height="80dp" android:background="@android:color/transparent" android:layout_alignParentRight="true" android:layout_alignParentBottom="true" android:src="@drawable/icon_video_play"/> </RelativeLayout> <Button android:id="@+id/btn_ok" android:layout_width="wrap_content" android:layout_height="55dip" android:layout_gravity="center" android:text="确定"/> </LinearLayout>
视频播放界面
<?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="com.gyq.recordvideotest.PlayerActivity"> <cn.jzvd.JZVideoPlayerStandard android:id="@+id/video_player" android:layout_width="match_parent" android:layout_height="200dp"/> </RelativeLayout>
4).实现录制视频
package com.gyq.recordvideotest; import android.content.Intent; import android.graphics.PixelFormat; import android.hardware.Camera; import android.media.MediaRecorder; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.widget.Chronometer; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.Toast; import com.gyq.recordvideotest.utils.SPUtils; import com.othershe.nicedialog.BaseNiceDialog; import com.othershe.nicedialog.NiceDialog; import com.othershe.nicedialog.ViewConvertListener; import com.othershe.nicedialog.ViewHolder; import java.io.File; import java.util.Collections; import java.util.Comparator; import java.util.List; public class MainActivity extends AppCompatActivity { private LinearLayout mRecord; private SurfaceView mSurfaceView; private SurfaceHolder mSurfaceHolder; private ImageButton mStartStop; private boolean isRecording = false;//标记是否已经在录制 private MediaRecorder mRecorder;//音视频录制类 private Camera mCamera = null;//相机 private Camera.Size mSize = null;//相机的尺寸 private int mCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK;//默认后置摄像头 private Chronometer mTimer; private SPUtils mSp; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mSp = new SPUtils("VIDEO",this); mRecord = (LinearLayout)findViewById(R.id.ll_click_record); mRecord.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { NiceDialog.init() .setLayoutId(R.layout.video_record_layout) .setConvertListener(new ViewConvertListener() { @Override public void convertView(final ViewHolder holder, final BaseNiceDialog dialog) { mSurfaceView = (SurfaceView) holder.getConvertView().findViewById(R.id.surface_view); mStartStop = (ImageButton) holder.getConvertView().findViewById(R.id.ib_play); mSurfaceView.setZOrderOnTop(true); mSurfaceView.setZOrderMediaOverlay(true); mTimer = (Chronometer) holder.getConvertView().findViewById(R.id.timer); holder.setOnClickListener(R.id.ib_play, new View.OnClickListener() { @Override public void onClick(View v) { //dialog.dismiss(); if (!isRecording) { startRecord(); } else { stopRecord(); Toast.makeText(MainActivity.this,"视频已保存", Toast.LENGTH_SHORT).show(); dialog.dismiss(); } } }); final SurfaceHolder holder2 = mSurfaceView.getHolder();// 取得holder holder2.setFormat(PixelFormat.TRANSPARENT); holder2.setFixedSize(1280,720); holder2.setKeepScreenOn(true); holder2.addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { // 将holder,这个holder为开始在onCreate里面取得的holder,将它赋给mSurfaceHolder mSurfaceHolder = holder2; if (mCamera == null) { return; } try { //设置显示 mCamera.setPreviewDisplay(holder2); mCamera.startPreview(); } catch (Exception e) { e.printStackTrace(); releaseCamera(); finish(); } } @Override public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { // 将holder,这个holder为开始在onCreate里面取得的holder,将它赋给mSurfaceHolder mSurfaceHolder = holder2; } @Override public void surfaceDestroyed(SurfaceHolder surfaceHolder) { // surfaceDestroyed的时候同时对象设置为null if (isRecording && mCamera != null) { mCamera.lock(); } mSurfaceView = null; mSurfaceHolder = null; releaseMediaRecorder(); releaseCamera(); } }); // holder加入回调接口 } }) .setWidth(800) .setHeight(500) .setOutCancel(false) .show(getSupportFragmentManager()); } }); } /** * 初始化相机 */ private void initCamera() { if (Camera.getNumberOfCameras() == 2) { mCamera = Camera.open(mCameraFacing); } else { mCamera = Camera.open(); } CameraSizeComparator sizeComparator = new CameraSizeComparator(); Camera.Parameters parameters = mCamera.getParameters(); if (mSize == null) { List<Camera.Size> vSizeList = parameters.getSupportedPreviewSizes(); Collections.sort(vSizeList, sizeComparator); for (int num = 0; num < vSizeList.size(); num++) { Camera.Size size = vSizeList.get(num); if (size.width >= 800 && size.height >= 480) { this.mSize = size; break; } } mSize = vSizeList.get(0); List<String> focusModesList = parameters.getSupportedFocusModes(); //增加对聚焦模式的判断 if (focusModesList.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); } else if (focusModesList.contains(Camera.Parameters.FOCUS_MODE_AUTO)) { parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); } mCamera.setParameters(parameters); } int rotation = getWindowManager().getDefaultDisplay().getRotation(); } @Override protected void onResume() { super.onResume(); initCamera(); } @Override public void onPause() { releaseCamera(); super.onPause(); } /** * 停止录制 */ private void stopRecord() { try { //停止录制 mRecorder.stop(); mTimer.stop(); //重置 mRecorder.reset(); mStartStop.setImageResource(R.drawable.icon_video_play); } catch (Exception e) { e.printStackTrace(); } isRecording = false; } /** * 释放MediaRecorder */ private void releaseMediaRecorder() { if (mRecorder != null) { mRecorder.release(); mRecorder = null; } } /** * 释放相机资源 */ private void releaseCamera() { try { if (mCamera != null) { mCamera.stopPreview(); mCamera.setPreviewCallback(null); mCamera.unlock(); mCamera.release(); } } catch (RuntimeException e) { } finally { mCamera = null; } } /** * 开始录制 */ private void startRecord() { mTimer.start(); if (mRecorder == null) { mRecorder = new MediaRecorder(); // 创建MediaRecorder } if (mCamera != null) { mCamera.stopPreview(); mCamera.unlock(); mRecorder.setCamera(mCamera); } try { // 设置音频采集方式 mRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); //设置视频的采集方式 mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); //设置文件的输出格式 mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);//aac_adif, aac_adts, output_format_rtp_avp, output_format_mpeg2ts ,webm //设置audio的编码格式 mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); //设置video的编码格式 mRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); //设置录制的视频编码比特率 //mRecorder.setVideoEncodingBitRate(1024 * 1024); mRecorder.setVideoSize(1280,720); //设置录制的视频帧率,注意文档的说明: mRecorder.setVideoFrameRate(20); //设置要捕获的视频的宽度和高度 // mSurfaceHolder.setFixedSize(320, 240);//最高只能设置640x480 //mRecorder.setVideoSize(320, 240);//最高只能设置640x480 //设置记录会话的最大持续时间(毫秒) //mRecorder.setMaxDuration(60 * 1000); mRecorder.setPreviewDisplay(mSurfaceHolder.getSurface()); String path = getExternalCacheDir().getPath(); if (path != null) { File dir = new File(path + "/videos"); if (!dir.exists()) { dir.mkdir(); } path = dir + "/" + "SmartDoor_video"+System.currentTimeMillis() + ".mp4"; mSp.put("video_path",path); //设置输出文件的路径 mRecorder.setOutputFile(path); //准备录制 mRecorder.prepare(); //开始录制 mRecorder.start(); isRecording = true; //btnStartStop.setText("停止"); mStartStop.setImageResource(R.drawable.icon_video_stop); } } catch (Exception e) { e.printStackTrace(); } } private class CameraSizeComparator implements Comparator<Camera.Size> { public int compare(Camera.Size lhs, Camera.Size rhs) { if (lhs.width == rhs.width) { return 0; } else if (lhs.width > rhs.width) { return 1; } else { return -1; } } } public void play(View view) { Intent intent = new Intent(this,PlayerActivity.class); startActivity(intent); } }
工具类 SPUtils.java
package com.gyq.recordvideotest.utils; import android.content.Context; import android.content.SharedPreferences; import android.support.annotation.Nullable; import java.util.Map; import java.util.Set; /** * Created by gyq on 2017/7/27 14:18 */ public class SPUtils { private SharedPreferences sp; private SharedPreferences.Editor editor; /** * SPUtils构造函数 * <p>在Application中初始化</p> * * @param spName spName */ public SPUtils(String spName, Context context) { sp = context.getSharedPreferences(spName, Context.MODE_PRIVATE); editor = sp.edit(); editor.apply(); } /** * SP中写入String类型value * * @param key 键 * @param value 值 */ public void put(String key, @Nullable String value) { editor.putString(key, value).apply(); } /** * SP中读取String * * @param key 键 * @return 存在返回对应值,不存在返回默认值{@code null} */ public String getString(String key) { return getString(key, null); } /** * SP中读取String * * @param key 键 * @param defaultValue 默认值 * @return 存在返回对应值,不存在返回默认值{@code defaultValue} */ public String getString(String key, String defaultValue) { return sp.getString(key, defaultValue); } /** * SP中写入int类型value * * @param key 键 * @param value 值 */ public void put(String key, int value) { editor.putInt(key, value).apply(); } /** * SP中读取int * * @param key 键 * @return 存在返回对应值,不存在返回默认值-1 */ public int getInt(String key) { return getInt(key, -1); } /** * SP中读取int * * @param key 键 * @param defaultValue 默认值 * @return 存在返回对应值,不存在返回默认值{@code defaultValue} */ public int getInt(String key, int defaultValue) { return sp.getInt(key, defaultValue); } /** * SP中写入long类型value * * @param key 键 * @param value 值 */ public void put(String key, long value) { editor.putLong(key, value).apply(); } /** * SP中读取long * * @param key 键 * @return 存在返回对应值,不存在返回默认值-1 */ public long getLong(String key) { return getLong(key, -1L); } /** * SP中读取long * * @param key 键 * @param defaultValue 默认值 * @return 存在返回对应值,不存在返回默认值{@code defaultValue} */ public long getLong(String key, long defaultValue) { return sp.getLong(key, defaultValue); } /** * SP中写入float类型value * * @param key 键 * @param value 值 */ public void put(String key, float value) { editor.putFloat(key, value).apply(); } /** * SP中读取float * * @param key 键 * @return 存在返回对应值,不存在返回默认值-1 */ public float getFloat(String key) { return getFloat(key, -1f); } /** * SP中读取float * * @param key 键 * @param defaultValue 默认值 * @return 存在返回对应值,不存在返回默认值{@code defaultValue} */ public float getFloat(String key, float defaultValue) { return sp.getFloat(key, defaultValue); } /** * SP中写入boolean类型value * * @param key 键 * @param value 值 */ public void put(String key, boolean value) { editor.putBoolean(key, value).apply(); } /** * SP中读取boolean * * @param key 键 * @return 存在返回对应值,不存在返回默认值{@code false} */ public boolean getBoolean(String key) { return getBoolean(key, false); } /** * SP中读取boolean * * @param key 键 * @param defaultValue 默认值 * @return 存在返回对应值,不存在返回默认值{@code defaultValue} */ public boolean getBoolean(String key, boolean defaultValue) { return sp.getBoolean(key, defaultValue); } /** * SP中写入String集合类型value * * @param key 键 * @param values 值 */ public void put(String key, @Nullable Set<String> values) { editor.putStringSet(key, values).apply(); } /** * SP中读取StringSet * * @param key 键 * @return 存在返回对应值,不存在返回默认值{@code null} */ public Set<String> getStringSet(String key) { return getStringSet(key, null); } /** * SP中读取StringSet * * @param key 键 * @param defaultValue 默认值 * @return 存在返回对应值,不存在返回默认值{@code defaultValue} */ public Set<String> getStringSet(String key, @Nullable Set<String> defaultValue) { return sp.getStringSet(key, defaultValue); } /** * SP中获取所有键值对 * * @return Map对象 */ public Map<String, ?> getAll() { return sp.getAll(); } /** * SP中移除该key * * @param key 键 */ public void remove(String key) { editor.remove(key).apply(); } /** * SP中是否存在该key * * @param key 键 * @return {@code true}: 存在<br>{@code false}: 不存在 */ public boolean contains(String key) { return sp.contains(key); } /** * SP中清除所有数据 */ public void clear() { editor.clear().apply(); } }
5).播放视频
package com.gyq.recordvideotest; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; import com.gyq.recordvideotest.utils.SPUtils; import cn.jzvd.JZUserAction; import cn.jzvd.JZUserActionStandard; import cn.jzvd.JZVideoPlayer; import cn.jzvd.JZVideoPlayerStandard; public class PlayerActivity extends AppCompatActivity { private JZVideoPlayerStandard mplayer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_player); SPUtils msp = new SPUtils("VIDEO",this); mplayer = (JZVideoPlayerStandard)findViewById(R.id.video_player); mplayer.setUp(msp.getString("video_path"),JZVideoPlayerStandard.SCREEN_LAYOUT_NORMAL,"留言"); JZVideoPlayer.setJzUserAction(new MyUserActionStandard()); } @Override protected void onPause() { super.onPause(); JZVideoPlayer.releaseAllVideos(); } @Override public void onBackPressed() { if (JZVideoPlayer.backPress()) { return; } super.onBackPressed(); } class MyUserActionStandard implements JZUserActionStandard { @Override public void onEvent(int type, String url, int screen, Object... objects) { switch (type) { case JZUserAction.ON_CLICK_START_ICON: Log.i("USER_EVENT", "ON_CLICK_START_ICON" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserAction.ON_CLICK_START_ERROR: Log.i("USER_EVENT", "ON_CLICK_START_ERROR" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserAction.ON_CLICK_START_AUTO_COMPLETE: Log.i("USER_EVENT", "ON_CLICK_START_AUTO_COMPLETE" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserAction.ON_CLICK_PAUSE: Log.i("USER_EVENT", "ON_CLICK_PAUSE" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserAction.ON_CLICK_RESUME: Log.i("USER_EVENT", "ON_CLICK_RESUME" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserAction.ON_SEEK_POSITION: Log.i("USER_EVENT", "ON_SEEK_POSITION" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserAction.ON_AUTO_COMPLETE: Log.i("USER_EVENT", "ON_AUTO_COMPLETE" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserAction.ON_ENTER_FULLSCREEN: Log.i("USER_EVENT", "ON_ENTER_FULLSCREEN" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserAction.ON_QUIT_FULLSCREEN: Log.i("USER_EVENT", "ON_QUIT_FULLSCREEN" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserAction.ON_ENTER_TINYSCREEN: Log.i("USER_EVENT", "ON_ENTER_TINYSCREEN" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserAction.ON_QUIT_TINYSCREEN: Log.i("USER_EVENT", "ON_QUIT_TINYSCREEN" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserAction.ON_TOUCH_SCREEN_SEEK_VOLUME: Log.i("USER_EVENT", "ON_TOUCH_SCREEN_SEEK_VOLUME" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserAction.ON_TOUCH_SCREEN_SEEK_POSITION: Log.i("USER_EVENT", "ON_TOUCH_SCREEN_SEEK_POSITION" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserActionStandard.ON_CLICK_START_THUMB: Log.i("USER_EVENT", "ON_CLICK_START_THUMB" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; case JZUserActionStandard.ON_CLICK_BLANK: Log.i("USER_EVENT", "ON_CLICK_BLANK" + " title is : " + (objects.length == 0 ? "" : objects[0]) + " url is : " + url + " screen is : " + screen); break; default: Log.i("USER_EVENT", "unknow"); break; } } } }
介绍一个很好用的视频播放 JiaoZiVideoPlayer
相关文章推荐
- 全屏录制播放控件--Android 开发中关于视频录制和播放的诸多问题处理
- Android开发笔记——视频录制播放常见问题
- Android开发笔记——视频录制播放常见问题
- Android开发笔记——视频录制播放常见问题
- iOS开发 音频播放、录音、视频播放、拍照、视频录制
- iOS开发系列--音频播放、录音、视频播放、拍照、视频录制
- iOS开发系列--音频播放、录音、视频播放、拍照、视频录制
- iOS开发系列--音频播放、录音、视频播放、拍照、视频录制
- android仿微信录制短视频并播放
- android录制和播放视频
- 【Android开发VR实战】二.播放360°全景视频
- [Android 开发]webview播放优酷等视频 无法播放怎么办?
- android开发视频播放相关使用
- Android 实现视频录制并播放
- iOS开发系列--音频播放、录音、视频播放、拍照、视频录制
- Android开发之基础---------多媒体音乐和视频播放
- Android开发 之 简单视频播放
- iOS开发系列–音频播放、录音、视频播放、拍照、视频录制
- iOS开发系列--音频播放、录音、视频播放、拍照、视频录制
- 本人讲课时录制的Android应用开发技术教学视频