老大下达了一个要求,让我高仿一个类似微信录制小视频的功能,根据网上各路大神的demo及思路参考下,然后我就粗糙的做了一个小demo,个人感觉还不错,不喜者忽喷。源码链接在文件后面提供,欢迎大家学习及参考,有不足之处请跟我联系,我会加紧改进,一起探讨学习,希望能帮助那些需要实现类似功能的小伙伴们。话不多说,直接上效果图:
- 主界面图
- 短视频界面图
- 短视频录制效果图
不能发视频效果,只能是图片,就用工具压缩成了gif,吖这画质我也是醉醉的,就将就着看看吧,实际很清晰的,还会自动对焦。
实现代码过长就不全贴了,文件中有两个依赖库,一个是某大神的录制库,一个是金山云的播放库,效果挺好的,大家有兴趣就下载源文件看吧
重点代码:MainActivity.java
package com.example.jerei.videodemo;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.media.ThumbnailUtils;
import android.provider.MediaStore;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.sh.shvideolibrary.VideoInputActivity;
import com.sh.shvideolibrary.VideoInputDialog;
import com.sh.shvideolibrary.compression.CompressListener;
import com.sh.shvideolibrary.compression.CompressorUtils;
import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MainActivity extends AppCompatActivity implements VideoInputDialog.VideoCall {
private static final int REQUEST_CODE_FOR_PLAYER = 4000;
Button button;
Button button2;
TextView first;
TextView back;
ProgressBar progressBar;
static String TAG = "MainActivity";
String path;//视频录制输出地址
//视频压缩数据地址
private String currentOutputVideoPath;
private static final int REQUEST_CODE_FOR_RECORD_VIDEO = 5230;//录制视频请求码
Double videoLength = 0.0;//视频时长
private Button btnPlay;
private String finishPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.button);
button2 = (Button) findViewById(R.id.button2);
first = (TextView) findViewById(R.id.first);
btnPlay = ((Button) findViewById(R.id.btnPlay));
back = (TextView) findViewById(R.id.back);
progressBar = (ProgressBar) findViewById(R.id.progressBar);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
btnPlay.setEnabled(false);
//显示视频录制控件
VideoInputDialog.show(getSupportFragmentManager(), MainActivity.this, VideoInputDialog.Q720, MainActivity.this);
}
});
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
btnPlay.setEnabled(false);
VideoInputActivity.startActivityForResult(MainActivity.this, REQUEST_CODE_FOR_RECORD_VIDEO, VideoInputActivity.Q720);
}
});
btnPlay.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
breakPlayer();
}
});
}
private void breakPlayer() {
Intent intent = new Intent(MainActivity.this, TextureVideoActivity.class);
Log.e(TAG, "跳转path=" + finishPath);
intent.putExtra("path", finishPath);
startActivityForResult(intent, REQUEST_CODE_FOR_PLAYER);
}
/**
* 小视屏录制回调
*
* @param path
*/
@Override
public void videoPathCall(String path) {
Log.e("地址:", path);
//根据视频地址获取缩略图
this.path = path;
first.setText(getFileSize(path));
finishPath = path;
}
@Override
public void videoFinish(boolean isFinish) {
if (isFinish) {
final File mediaFile = VideoInputDialog.getOutputMediaFile();
currentOutputVideoPath = mediaFile.getAbsolutePath();
breakPlayer();
btnPlay.setEnabled(true);
//获取视频时长 计算压缩进度用
// ysVideo(mediaFile);
}
}
private void ysVideo(final File mediaFile) {
MediaMetadataRetriever retr = new MediaMetadataRetriever();
retr.setDataSource(path);
String time = retr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);//获取视频时长
//7680
try {
videoLength = Double.parseDouble(time) / 1000.00;
} catch (Exception e) {
e.printStackTrace();
videoLength = 0.00;
}
Log.v(TAG, "videoLength = " + videoLength + "s");
/**
* 压缩视频
*/
CompressorUtils compressorUtils = new CompressorUtils(path, mediaFile.getAbsolutePath(), MainActivity.this);
compressorUtils.execCommand(new CompressListener() {
@Override
public void onExecSuccess(String message) {
Log.i(TAG, "success " + message);
progressBar.setVisibility(View.INVISIBLE);
textAppend(getString(R.string.compress_succeed));
back.setText(getFileSize(mediaFile.getAbsolutePath()));
//获取缩略图
Bitmap bitmap = ThumbnailUtils.createVideoThumbnail(mediaFile.getAbsolutePath(), MediaStore.Video.Thumbnails.MINI_KIND);
File file = new File(path);
if (file.isFile()) {
file.delete();
}
finishPath = mediaFile.getAbsolutePath();
btnPlay.setEnabled(true);
}
@Override
public void onExecFail(String reason) {
Log.i(TAG, "fail " + reason);
}
@Override
public void onExecProgress(String message) {
progressBar.setVisibility(View.VISIBLE);
textAppend(getString(R.string.compress_progress, message));
int i = getProgress(message);
Log.e("进度", i + "");
progressBar.setProgress(i);
}
});
}
/**
* 录制视频回调
*
* @param requestCode
* @param resultCode
* @param data
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_FOR_RECORD_VIDEO && resultCode == RESULT_CANCELED) {
}
if (requestCode == REQUEST_CODE_FOR_PLAYER && resultCode == RESULT_OK) {
btnPlay.setEnabled(false);
}
if (requestCode == REQUEST_CODE_FOR_RECORD_VIDEO && resultCode == RESULT_OK) {
String path = data.getStringExtra(VideoInputActivity.INTENT_EXTRA_VIDEO_PATH);
Log.e("地址:", path);
//根据视频地址获取缩略图
this.path = path;
Bitmap bitmap = ThumbnailUtils.createVideoThumbnail(path, MediaStore.Video.Thumbnails.MINI_KIND);
first.setText(getFileSize(path));
finishPath=path;
breakPlayer();
btnPlay.setEnabled(true);
// ysVideo(VideoInputDialog.getOutputMediaFile());
}
super.onActivityResult(requestCode, resultCode, data);
}
private String getFileSize(String path) {
File f = new File(path);
if (!f.exists()) {
return "0 MB";
} else {
long size = f.length();
return (size / 1024f) / 1024f + "MB";
}
}
int progress = 0;
private int getProgress(String source) {
// Duration: 00:00:22.50, start: 0.000000, bitrate: 13995 kb/s
//progress frame= 28 fps=0.0 q=24.0 size= 107kB time=00:00:00.91 bitrate= 956.4kbits/s
if (source.contains("start: 0.000000")) {
return progress;
}
Pattern p = Pattern.compile("00:\\d{2}:\\d{2}");
Matcher m = p.matcher(source);
if (m.find()) {
//00:00:00
String result = m.group(0);
String temp[] = result.split(":");
Double seconds = Double.parseDouble(temp[1]) * 60 + Double.parseDouble(temp[2]);
if (0 != videoLength) {
Log.v("进度长度", "current second = " + seconds + "/videoLength=" + videoLength);
progress = (int) (seconds * 100 / videoLength);
return progress;
}
return progress;
}
return progress;
}
private void textAppend(String text) {
if (!TextUtils.isEmpty(text)) {
Log.e("日志", text);
}
}
}
**
- 布局文件:activity_main.xml
**
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<Button
android:id="@+id/button"
android:layout_height="60dp"
android:layout_width="match_parent"
android:text="录制短视频"/>
<Button
android:id="@+id/button2"
android:layout_height="60dp"
android:layout_width="match_parent"
android:text="录制视频"/>
<!-- 定义一个水平进度条 -->
<ProgressBar android:layout_width="fill_parent"
android:visibility="invisible"
android:layout_height="wrap_content"
android:max="100"
android:id="@+id/progressBar"
style="@android:style/Widget.DeviceDefault.ProgressBar.Horizontal"/>
<TextView
android:gravity="center"
android:text="压缩前"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/first"
android:gravity="center"
android:text=""
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:gravity="center"
android:text="压缩后"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:gravity="center"
android:id="@+id/back"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnPlay"
android:layout_height="60dp"
android:layout_width="match_parent"
android:text="播放视频"
android:visibility="visible"
android:enabled="false"/>
</LinearLayout>
视频Recorder时的重点顺序,根据官网文档要求顺序最好不要颠倒了否则会报错,在VideoInputDialog类中为了提高录制效果及画质我将 设置了以下属性: mMediaRecorder.setVideoSize(800, 480);// 视频尺寸
mMediaRecorder.setVideoFrameRate(30);// 视频帧频率,这个可忽略,没什么效果
mMediaRecorder.setVideoEncodingBitRate(3 * 1024 * 1024);//越大画质越好但是视频越大
//初始化 mMediaRecorder 用于录像
private boolean prepareVideoRecorder(){
if (mCamera==null)
return false;
mMediaRecorder = new MediaRecorder();
/**
* 解锁camera
* 设置输出格式为mpeg_4(mp4),此格式音频编码格式必须为AAC否则网页无法播放
*/
mCamera.unlock();
mMediaRecorder.setCamera(mCamera);
//声音
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
//视频
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
// 第3步:设置输出格式和编码格式(针对低于API Level 8版本)
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
//音频编码格式对应应为AAC
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
//视频编码格式对应应为H264
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
//路径
mMediaRecorder.setOutputFile(getOutputMediaFile().toString());
//设置分辨率为480P
// mMediaRecorder.setProfile(CamcorderProfile.get(quality));
mMediaRecorder.setVideoSize(800, 480);// 视频尺寸
mMediaRecorder.setVideoFrameRate(30);// 视频帧频率
mMediaRecorder.setVideoEncodingBitRate(3 * 1024 * 1024);
mMediaRecorder.setPreviewDisplay(mPreview.getHolder().getSurface());
try {
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
if (cameraFront) {
mMediaRecorder.setOrientationHint(270);
} else {
mMediaRecorder.setOrientationHint(90);
}
}
mMediaRecorder.prepare();
} catch (IllegalStateException e) {
Log.d(TAG, "IllegalStateException preparing MediaRecorder: " + e.getMessage());
releaseMediaRecorder();
return false;
} catch (IOException e) {
Log.d(TAG, "IOException preparing MediaRecorder: " + e.getMessage());
releaseMediaRecorder();
return false;
}
return true;
}
播放控件采用了金山云的,感觉他们家的效果很棒,可以调整拉升、旋转,大小设置等功能很齐全,用起来容易上手,个人感觉还不错,挺喜欢的,拿着demo修修改改,终于弄出了自己想要的效果
package com.example.jerei.videodemo;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.media.AudioManager;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Toast;
import com.ksyun.media.player.IMediaPlayer;
import com.ksyun.media.player.KSYMediaPlayer;
import com.ksyun.media.player.KSYTextureView;
import java.io.File;
import java.io.IOException;
public class TextureVideoActivity extends Activity implements View.OnClickListener{
private static final String TAG = "TextureVideoActivity";
private Context mContext;
KSYTextureView mVideoView = null;
private int mVideoWidth = 0;
private int mVideoHeight = 0;
private String mDataSource;
private IMediaPlayer.OnPreparedListener mOnPreparedListener = new IMediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(IMediaPlayer mp) {
Log.d("VideoPlayer", "OnPrepared");
mVideoWidth = mVideoView.getVideoWidth();
mVideoHeight = mVideoView.getVideoHeight();
// Set Video Scaling Mode
mVideoView.setVideoScalingMode(KSYMediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING);
//start player
mVideoView.start();
}
};
private IMediaPlayer.OnVideoSizeChangedListener mOnVideoSizeChangeListener = new IMediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(IMediaPlayer mp, int width, int height, int sarNum, int sarDen) {
if (mVideoWidth > 0 && mVideoHeight > 0) {
if (width != mVideoWidth || height != mVideoHeight) {
mVideoWidth = mp.getVideoWidth();
mVideoHeight = mp.getVideoHeight();
if (mVideoView != null)
mVideoView.setVideoScalingMode(KSYMediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING);
}
}
}
};
private IMediaPlayer.OnSeekCompleteListener mOnSeekCompletedListener = new IMediaPlayer.OnSeekCompleteListener() {
@Override
public void onSeekComplete(IMediaPlayer mp) {
Log.e(TAG, "onSeekComplete...............");
}
};
private IMediaPlayer.OnCompletionListener mOnCompletionListener = new IMediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(IMediaPlayer mp) {
//Toast.makeText(mContext, "OnCompletionListener, play complete.", Toast.LENGTH_LONG).show();
//videoPlayEnd();
mVideoView.start();
}
};
private IMediaPlayer.OnErrorListener mOnErrorListener = new IMediaPlayer.OnErrorListener() {
@Override
public boolean onError(IMediaPlayer mp, int what, int extra) {
switch (what) {
//case KSYVideoView.MEDIA_ERROR_UNKNOWN:
// Log.e(TAG, "OnErrorListener, Error Unknown:" + what + ",extra:" + extra);
// break;
default:
Log.e(TAG, "OnErrorListener, Error:" + what + ",extra:" + extra);
}
videoPlayEnd();
return false;
}
};
public IMediaPlayer.OnInfoListener mOnInfoListener = new IMediaPlayer.OnInfoListener() {
@Override
public boolean onInfo(IMediaPlayer iMediaPlayer, int i, int i1) {
switch (i) {
case KSYMediaPlayer.MEDIA_INFO_BUFFERING_START:
Log.d(TAG, "Buffering Start.");
break;
case KSYMediaPlayer.MEDIA_INFO_BUFFERING_END:
Log.d(TAG, "Buffering End.");
break;
case KSYMediaPlayer.MEDIA_INFO_AUDIO_RENDERING_START:
Toast.makeText(mContext, "Audio Rendering Start", Toast.LENGTH_SHORT).show();
break;
case KSYMediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START:
Toast.makeText(mContext, "Video Rendering Start", Toast.LENGTH_SHORT).show();
break;
case KSYMediaPlayer.MEDIA_INFO_SUGGEST_RELOAD:
// Player find a new stream(video or audio), and we could reload the video.
if (mVideoView != null)
mVideoView.reload(mDataSource, false, KSYMediaPlayer.KSYReloadMode.KSY_RELOAD_MODE_ACCURATE);
break;
case KSYMediaPlayer.MEDIA_INFO_RELOADED:
Toast.makeText(mContext, "Succeed to reload video.", Toast.LENGTH_SHORT).show();
Log.d(TAG, "Succeed to reload video.");
return false;
}
return false;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = this.getApplicationContext();
setContentView(R.layout.texture_player);
mVideoView = (KSYTextureView) findViewById(R.id.texture_view);
mVideoView.setKeepScreenOn(true);
this.setVolumeControlStream(AudioManager.STREAM_MUSIC);
mDataSource = getIntent().getStringExtra("path");
mVideoView.setOnCompletionListener(mOnCompletionListener);
mVideoView.setOnPreparedListener(mOnPreparedListener);
mVideoView.setOnInfoListener(mOnInfoListener);
mVideoView.setOnVideoSizeChangedListener(mOnVideoSizeChangeListener);
mVideoView.setOnErrorListener(mOnErrorListener);
mVideoView.setOnSeekCompleteListener(mOnSeekCompletedListener);
mVideoView.setScreenOnWhilePlaying(true);
mVideoView.setBufferTimeMax(3.0f);
mVideoView.setTimeout(5, 30);
mVideoView.setDecodeMode(KSYMediaPlayer.KSYDecodeMode.KSY_DECODE_MODE_AUTO);
try {
mVideoView.setDataSource(mDataSource);
} catch (IOException e) {
e.printStackTrace();
}
mVideoView.prepareAsync();
}
@Override
protected void onDestroy() {
super.onDestroy();
mVideoView = null;
}
@Override
protected void onPause() {
super.onPause();
if (mVideoView != null) {
mVideoView.runInBackground(true);
}
}
@Override
protected void onResume() {
super.onResume();
if (mVideoView != null) {
mVideoView.runInForeground();
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
videoPlayEnd();
}
return super.onKeyDown(keyCode, event);
}
@Override
public int getChangingConfigurations() {
return super.getChangingConfigurations();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
private void videoPlayEnd() {
if (mVideoView != null) {
mVideoView.release();
mVideoView = null;
}
finish();
}
@Override
public void onClick(View v) {
final Intent intent = getIntent();
switch (v.getId()){
case R.id.tv_del:
File file = new File(mDataSource);
if (file.isFile()) {
file.delete();
}
setResult(Activity.RESULT_OK, intent);
videoPlayEnd();
break;
case R.id.tv_relese:
Toast.makeText(TextureVideoActivity.this, "点击了发布按钮", Toast.LENGTH_SHORT).show();
//进行数据发布操作
setResult(Activity.RESULT_OK, intent);
videoPlayEnd();
break;
}
}
}
**
- 布局文件 :texture_player.xml
**
<?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"
android:background="@color/player_black">
<com.ksyun.media.player.KSYTextureView
android:id="@+id/texture_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
tools:targetApi="ice_cream_sandwich" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_del"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="20dp"
android:text="取消"
android:onClick="onClick"
android:gravity="center"
android:textColor="#fff"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_relese"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="20dp"
android:text="发布"
android:onClick="onClick"
android:gravity="center"
android:textColor="#fff"
android:textSize="16sp" />
</LinearLayout>
</RelativeLayout>