文章目录
- 版权声明
- 前言
- 1.Camera1和Camera2的区别
- 2.为什么选择Camera2?
- 一、应用Camera2+MediaPlayer实现拍摄功能
- 引入所需权限
- 构建UI界面的XML
- Activity中的代码部分
- 二、在上述界面录制结束后点击跳转新的界面进行视频播放
- 构建播放界面部分的XML
- Activity的代码
- 上述代码中的注释部分为上传功能留白。
- 三.视频上传功能的实现
- 0.集成Java上传SDK
- 1.application.yml的配置
- 2.Controller层
- 3.Service
- 4.Impl
- 5.工具类
- 在上述Android代码的留白部分加入Okhttp请求
- 效果
版权声明
1.在视频录制阶段用到的Camera2+MediaRecorder技术借鉴了下方博主的文章,给出原文链接和版权声明:
原文链接
2.在视频播放阶段用到的MediaPlayer技术借鉴了下方博主的文章,给出原文连接:
原文链接
前言
1.Camera1和Camera2的区别
Camera1和Camera2在多个方面存在显著的区别。
首先,从功能角度来看,Camera1主要支持一次拍摄一张图片,而Camera2则支持一次拍摄多张图片,甚至可以是多张格式和尺寸不同的图片。例如,使用Camera2,你可以同时拍摄一张1440x1080的JPEG图片和一张全尺寸的RAW图片。
其次,Camera2提供了更多的手动设置选项,如曝光时间、ISO感光度、焦距等,这使得用户能够更精细地控制拍摄过程。此外,Camera2还支持RAW图像捕获和高速连拍模式等新功能,进一步丰富了其拍摄能力。
在性能优化方面,Camera2 API支持并行拍摄和预览,这使得在同时进行多个操作时表现更好。相比之下,Camera1可能在处理并行任务时表现稍显不足。
在检查相机信息方面,Camera2引入了CameraCharacteristics实例,它专门提供相机信息,使得用户可以在不开启相机的前提下检查几乎所有的相机信息。而Camera1则无法在开机相机之前检查详细的相机信息。
从实现逻辑上看,Camera1的逻辑是面向Camera对象的,所有的行为都是基于这个对象的方法的。如果Camera对象本身存在问题,是无法感知的,只能在调用方法时抛出异常。而Camera2的逻辑则是面向状态的,无论是设备还是会话(Session),任何状态的改变都可以做出相应的处理。这种逻辑的不同使得Camera2在处理各种情况时更为灵活和高效。
此外,Camera2还是现在的多摄像头管理框架,可以更好地管理和协调多个摄像头的工作。而Camera1主要是以前的前后摄像头的处理方式。
综上所述,Camera2在功能、性能、信息检查以及实现逻辑等方面相较于Camera1有明显的提升和优化,更适合现代摄影和多媒体应用的需求。然而,具体使用哪种API还需要根据具体的应用场景和需求来决定。
2.为什么选择Camera2?
Camera1(通常指的是较旧的相机API)在功能和性能上相较于Camera2(即新的相机API)确实有所不足。Camera2提供了更多的手动设置选项、支持并行拍摄和预览、允许在不开启相机的前提下检查相机信息等。此外,Camera2还是现在的多摄像头管理框架,可以更好地管理和协调多个摄像头的工作。
较新版本的API可能不再支持Camera1中的一些方法。随着技术的不断发展和更新,新的API版本往往会引入新的功能和改进,同时可能会淘汰或替换旧的方法。
较新版本的API不再支持Camera1中的方法可能会涉及多个方面,具体取决于API的更新内容和目标设备的兼容性。以下是一些常见的情况和可能不再支持的方法:
相机打开和关闭:
openCamera(int):用于打开指定ID的相机。
release():用于释放相机资源。
相机参数设置:
getParameters() 和 setParameters(Camera.Parameters):用于获取和设置相机的参数,如闪光灯模式、预览大小、图片格式等。
setDisplayOrientation(int):用于设置相机预览的显示方向。
预览和拍摄:
setPreviewDisplay(SurfaceHolder):用于设置预览显示的Surface。
startPreview() 和 stopPreview():用于开始和停止预览。
takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback):用于拍摄照片并接收回调。
焦点和缩放:
autoFocus(Camera.AutoFocusCallback):用于自动对焦。
startSmoothZoom(int) 和 stopSmoothZoom():用于平滑缩放。
事件监听:
setPreviewCallback(Camera.PreviewCallback):用于接收预览帧的回调。
setErrorCallback(Camera.ErrorCallback):用于接收相机错误的回调。
其他:
unlock() 和 reconnect():与媒体录制器配合使用,用于在录制视频时解锁和重新连接相机。
需要注意的是,随着Android版本的更新和Camera2 API的引入,许多Camera1的方法被新的API所替代或重新设计。Camera2 API提供了更灵活、更强大的相机功能,并支持更多的手动控制和高级特性。因此,在开发新应用或更新现有应用时,建议迁移到Camera2 API以利用最新的功能和性能优势。
一、应用Camera2+MediaPlayer实现拍摄功能
引入所需权限
<uses-permission android:name="android.permission.INTERNET" /> <!-- 获取网络状态,根据网络状态切换进行数据请求网络转换 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><!-- 读取外置存储 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><!-- 写外置存储 -->
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
构建UI界面的XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".submit.Camera2">
<TextureView
android:id="@+id/textureView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<Button
android:id="@+id/btn_finish"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="结束"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
<Button
android:id="@+id/btn_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Activity中的代码部分
package com.example.travelassistant.submit;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Size;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;
import com.example.travelassistant.R;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
public class Camera2 extends AppCompatActivity {
private static final String TAG = Camera2.class.getSimpleName();
private Button mBtnStatr,mBtnFinish;
private TextureView mTextureView;
private CameraManager mCameraManager;
private CameraDevice mCameraDevice;
private CameraCaptureSession mCameraCaptureSession;
private CameraDevice.StateCallback mCameraDeviceStateCallback;
private CameraCaptureSession.StateCallback mSessionStateCallback;
private CameraCaptureSession.CaptureCallback mSessionCaptureCallback;
private CaptureRequest.Builder mPreviewCaptureRequest;
private CaptureRequest.Builder mRecorderCaptureRequest;
private MediaRecorder mMediaRecorder;
private String mCurrentSelectCamera;
private Handler mChildHandler;
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera2);
if(ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.RECORD_AUDIO}, 1);
}
if(ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.CAMERA}, 1);
}
if(ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}
mTextureView = findViewById(R.id.textureView);
mBtnStatr = findViewById(R.id.btn_start);
mBtnFinish = findViewById(R.id.btn_finish);
initClickListener();
initChildHandler();
initTextureViewStateListener();
initMediaRecorder();
initCameraDeviceStateCallback();
initSessionStateCallback();
initSessionCaptureCallback();
}
private void initClickListener(){
mBtnStatr.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
config();
startRecorder();
}
});
mBtnFinish.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
stopRecorder();
}
});
}
/**
* 初始化TextureView的纹理生成监听,只有纹理生成准备好了。我们才能去进行摄像头的初始化工作让TextureView接收摄像头预览画面
*/
private void initTextureViewStateListener(){
mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
//可以使用纹理
initCameraManager();
selectCamera();
openCamera();
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
//纹理尺寸变化
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
//纹理被销毁
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
//纹理更新
}
});
}
/**
* 初始化子线程Handler,操作Camera2需要一个子线程的Handler
*/
private void initChildHandler(){
HandlerThread handlerThread = new HandlerThread("Camera2Demo");
handlerThread.start();
mChildHandler = new Handler(handlerThread.getLooper());
}
/**
* 初始化MediaRecorder
*/
private void initMediaRecorder(){
mMediaRecorder = new MediaRecorder();
}
/**
* 配置录制视频相关数据
*/
private void configMediaRecorder(){
File file = new File(getExternalCacheDir(),"demo.mp4");
if (file.exists()){
file.delete();
}
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);//设置音频来源
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);//设置视频来源
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);//设置输出格式
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);//设置音频编码格式,请注意这里使用默认,实际app项目需要考虑兼容问题,应该选择AAC
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);//设置视频编码格式,请注意这里使用默认,实际app项目需要考虑兼容问题,应该选择H264
mMediaRecorder.setVideoEncodingBitRate(8*1024*1920);//设置比特率 一般是 1*分辨率 到 10*分辨率 之间波动。比特率越大视频越清晰但是视频文件也越大。
mMediaRecorder.setVideoFrameRate(30);//设置帧数 选择 30即可, 过大帧数也会让视频文件更大当然也会更流畅,但是没有多少实际提升。人眼极限也就30帧了。
Size size = getMatchingSize2();
mMediaRecorder.setVideoSize(size.getWidth(),size.getHeight());
mMediaRecorder.setOrientationHint(90);
Surface surface = new Surface(mTextureView.getSurfaceTexture());
mMediaRecorder.setPreviewDisplay(surface);
mMediaRecorder.setOutputFile(file.getAbsolutePath());
try {
mMediaRecorder.prepare();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 重新配置录制视频时的CameraCaptureSession
*/
private void config(){
try {
mCameraCaptureSession.stopRepeating();//停止预览,准备切换到录制视频
mCameraCaptureSession.close();//关闭预览的会话,需要重新创建录制视频的会话
mCameraCaptureSession = null;
} catch (CameraAccessException e) {
e.printStackTrace();
}
configMediaRecorder();
Size cameraSize = getMatchingSize2();
SurfaceTexture surfaceTexture = mTextureView.getSurfaceTexture();
surfaceTexture.setDefaultBufferSize(cameraSize.getWidth(),cameraSize.getHeight());
Surface previewSurface = new Surface(surfaceTexture);
Surface recorderSurface = mMediaRecorder.getSurface();//从获取录制视频需要的Surface
try {
mPreviewCaptureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewCaptureRequest.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
mPreviewCaptureRequest.addTarget(previewSurface);
mPreviewCaptureRequest.addTarget(recorderSurface);
//请注意这里设置了Arrays.asList(previewSurface,recorderSurface) 2个Surface,很好理解录制视频也需要有画面预览,第一个是预览的Surface,第二个是录制视频使用的Surface
mCameraDevice.createCaptureSession(Arrays.asList(previewSurface,recorderSurface),mSessionStateCallback,mChildHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
/**
* 开始录制视频
*/
private void startRecorder(){
mMediaRecorder.start();
}
/**
* 暂停录制视频(暂停后视频文件会自动保存)
*/
private void stopRecorder(){
mMediaRecorder.stop();
mMediaRecorder.reset();
Intent intent = new Intent(Camera2.this, PlayVideoActivity.class);
startActivity(intent);
}
/**
* 初始化Camera2的相机管理,CameraManager用于获取摄像头分辨率,摄像头方向,摄像头id与打开摄像头的工作
*/
private void initCameraManager(){
mCameraManager = (CameraManager)getSystemService(Context.CAMERA_SERVICE);
}
/**
* 选择一颗我们需要使用的摄像头,主要是选择使用前摄还是后摄或者是外接摄像头
*/
private void selectCamera(){
if (mCameraManager != null) {
Log.e(TAG, "selectCamera: CameraManager is null");
}
try {
String[] cameraIdList = mCameraManager.getCameraIdList(); //获取当前设备的全部摄像头id集合
if (cameraIdList.length == 0){
Log.e(TAG, "selectCamera: cameraIdList length is 0");
}
for (String cameraId : cameraIdList){ //遍历所有摄像头
CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);//得到当前id的摄像头描述特征
Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING); //获取摄像头的方向特征信息
if (facing == CameraCharacteristics.LENS_FACING_BACK){ //这里选择了后摄像头
mCurrentSelectCamera = cameraId;
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private void initCameraDeviceStateCallback(){
mCameraDeviceStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
//摄像头被打开
try {
mCameraDevice = camera;
Size cameraSize = getMatchingSize2();//计算获取需要的摄像头分辨率
SurfaceTexture surfaceTexture = mTextureView.getSurfaceTexture();//得到纹理
surfaceTexture.setDefaultBufferSize(cameraSize.getWidth(),cameraSize.getHeight());
Surface previewSurface = new Surface(surfaceTexture);
mPreviewCaptureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewCaptureRequest.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
mPreviewCaptureRequest.addTarget(previewSurface);
mCameraDevice.createCaptureSession(Arrays.asList(previewSurface),mSessionStateCallback,mChildHandler);//创建数据捕获会话,用于摄像头画面预览,这里需要等待mSessionStateCallback回调
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
//摄像头断开
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
//异常
}
};
}
private void initSessionStateCallback(){
mSessionStateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mCameraCaptureSession = session;
try {
//执行重复获取数据请求,等于一直获取数据呈现预览画面,mSessionCaptureCallback会返回此次操作的信息回调
mCameraCaptureSession.setRepeatingRequest(mPreviewCaptureRequest.build(),mSessionCaptureCallback,mChildHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
}
};
}
private void initSessionCaptureCallback(){
mSessionCaptureCallback=new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) {
super.onCaptureStarted(session, request, timestamp, frameNumber);
}
@Override
public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
super.onCaptureProgressed(session, request, partialResult);
}
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
}
@Override
public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
super.onCaptureFailed(session, request, failure);
}
}
;
}
/**
* 打开摄像头,这里打开摄像头后,我们需要等待mCameraDeviceStateCallback的回调
*/
@SuppressLint("MissingPermission")
private void openCamera(){
try {
mCameraManager.openCamera(mCurrentSelectCamera,mCameraDeviceStateCallback,mChildHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
/**
* 计算需要的使用的摄像头分辨率
* @return
*/
private Size getMatchingSize2(){
Size selectSize = null;
try {
CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(mCurrentSelectCamera);
StreamConfigurationMap streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Size[] sizes = streamConfigurationMap.getOutputSizes(ImageFormat.JPEG);
DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); //因为我这里是将预览铺满屏幕,所以直接获取屏幕分辨率
int deviceWidth = displayMetrics.widthPixels; //屏幕分辨率宽
int deviceHeigh = displayMetrics.heightPixels; //屏幕分辨率高
Log.e(TAG, "getMatchingSize2: 屏幕密度宽度="+deviceWidth);
Log.e(TAG, "getMatchingSize2: 屏幕密度高度="+deviceHeigh );
/**
* 循环40次,让宽度范围从最小逐步增加,找到最符合屏幕宽度的分辨率,
* 你要是不放心那就增加循环,肯定会找到一个分辨率,不会出现此方法返回一个null的Size的情况
* ,但是循环越大后获取的分辨率就越不匹配
*/
Integer s=sizes.length;
selectSize=sizes[s-1];
} catch (CameraAccessException e) {
e.printStackTrace();
}
Log.e(TAG, "getMatchingSize2: 选择的分辨率宽度="+selectSize.getWidth());
Log.e(TAG, "getMatchingSize2: 选择的分辨率高度="+selectSize.getHeight());
return selectSize;
}
}
二、在上述界面录制结束后点击跳转新的界面进行视频播放
构建播放界面部分的XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".submit.PlayVideoActivity">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_weight="10"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:rotation="90"
/>
<!--水平线性布局-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal">
<!--播放按钮-->
<Button
android:id="@+id/play1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="播放"/>
<!--暂停按钮-->
<Button
android:id="@+id/pause1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="取消" />
<!--停止按钮-->
<Button
android:id="@+id/stop1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="上传" />
</LinearLayout>
</LinearLayout>
Activity的代码
package com.example.travelassistant.submit;
import androidx.appcompat.app.AppCompatActivity;
import android.media.AudioManager;
import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.Toast;
import com.example.travelassistant.R;
import com.example.travelassistant.utils.AsyncResponse;
import com.example.travelassistant.utils.SendRequest;
import java.io.File;
import java.io.IOException;
public class PlayVideoActivity extends AppCompatActivity {
private SurfaceView surfaceView;
private Button play, pause, stop;
private MediaPlayer mediaPlayer;
private SurfaceHolder surfaceHolder;
private boolean noPlay = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_play_video);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
play = (Button) findViewById(R.id.play1);
pause = (Button) findViewById(R.id.pause1);
stop = (Button) findViewById(R.id.stop1);
surfaceView = (SurfaceView) findViewById(R.id.surfaceView);
surfaceHolder = surfaceView.getHolder();
pause.setEnabled(false);
stop.setEnabled(false);
/**
* 实现播放功能
*/
play.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (noPlay) {
play();
noPlay = false;
} else {
mediaPlayer.start();
}
}
});
/**
* 实现取消功能
*/
pause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mediaPlayer.stop();
mediaPlayer.release();
//返回主页面
if (mediaPlayer.isPlaying()) {
mediaPlayer.pause();
}
}
});
/**
* 实现上传功能
*/
stop.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
if (mediaPlayer.isPlaying()) {
mediaPlayer.stop();
mediaPlayer.release();
noPlay = true;
pause.setEnabled(false);
stop.setEnabled(false);
}
File file=new File(getExternalCacheDir()+"/demo.mp4");
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
//这个位置实现视频上传的功能
@Override
public void processFailure() {
}
});
}
});
}
/**
* 创建play()方法,在该方法中实现视频的播放功能
*/
public void play() {
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDisplay(surfaceHolder);
try {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(getExternalCacheDir()+"/demo.mp4");
String rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
System.out.println(rotation);
mediaPlayer.setDataSource(getExternalCacheDir()+"/demo.mp4");
System.out.println(getExternalCacheDir()+"/demo.mp4");
// mediaPlayer.setDataSource(Environment.getExternalStorageDirectory() + "/DCIM/Camera/video.mp4");
mediaPlayer.prepare();
} catch (Exception e) {
e.printStackTrace();
}
mediaPlayer.start();
pause.setEnabled(true);
stop.setEnabled(true);
// 为MediaPlayer对象添加完成事件监听器
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
Toast.makeText(PlayVideoActivity.this, "视频播放完毕!", Toast.LENGTH_SHORT).show();
}
});
}
/**
* 当前Activity销毁时,停止正在播放的视频,并释放MediaPlayer所占用的资源
*/
@Override
protected void onDestroy() {
super.onDestroy();
if (mediaPlayer != null) {
if (mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
// Activity销毁时停止播放,释放资源。不做这个操作,即使退出还是能听到视频播放的声音
mediaPlayer.release();
}
}
}
上述代码中的注释部分为上传功能留白。
三.视频上传功能的实现
在实现视频上传到阿里云VOD功能时,有两种思路:
1.因为是在Android客户端上实现此功能,可以采取客户端的方式,可以参考阿里云VOD的开发文档
Android客户端上传文件
2.服务端开发一个上传的接口,Android应用Okhttp向服务端发送上传文件的请求
本文主要采取了第二种思路,接下来具体介绍如何实现
0.集成Java上传SDK
根据官方文档的提示,引入相关依赖
下载Java语言的SDK和Demo将其中的aliyun-java-vod-upload-1.4.15.jar放到模块的resources包下
如下图所示:
添加依赖
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-vod</artifactId>
<version>2.16.11</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68.noneautotype</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20170516</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>com.aliyun.vod</groupId>
<artifactId>upload</artifactId>
<version>1.4.15</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/aliyun-java-vod-upload-1.4.15.jar</systemPath>
</dependency>
根据文档文档的提示:接下来实现视频文件的上传,并将视频中的某一帧作为封面图片
1.application.yml的配置
1和2中要从官网中获取 3表示上传文件 这里要设置的大一点
2.Controller层
//上传视频的同时将视频的某一帧作为封面上传
@ApiOperation("上传视频并取某帧作为视频的封面")
@PostMapping("upload")
public Map<String,Object> uploadVideoAndImage(@RequestParam("file") MultipartFile multipartFile)
{
String[] strings=vodService.uploadVideoAndImage(multipartFile);
HashMap<String, Object> map = new HashMap<>();
map.put("code","0");
map.put("message","成功");
map.put("data",strings);
System.out.println(strings);
return map;
}
3.Service
public interface VodService {
String[] uploadVideoAndImage(MultipartFile multipartFile);
}
4.Impl
@Override
public String[] uploadVideoAndImage(MultipartFile multipartFile) {
String[] strings = new String[3];
try {
InputStream inputStream = multipartFile.getInputStream();
final int fifthFrame= 10;
FFmpegFrameGrabber grabber;
grabber = new FFmpegFrameGrabber(inputStream);
grabber.start();
// 视频总帧数
int videoLength = grabber.getLengthInFrames();
Frame frame = null;
int i = 0;
while (i < videoLength) {
// 过滤前5帧,因为前5帧可能是全黑的
frame = grabber.grabFrame();
if ((i > fifthFrame) && (frame.image != null)) {
break;
}
i++;
}
Java2DFrameConverter converter = new Java2DFrameConverter();
// 绘制图片
BufferedImage bi = converter.getBufferedImage(frame);
BufferedImage rotatedImage = ImageRotation.rotateImage(bi, 90);
grabber.stop();
grabber.close();
//将图片转换成输入流
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(rotatedImage, "png", os);
InputStream input = new ByteArrayInputStream(os.toByteArray());
//绘制完图片后 直接以流的方式将封面图片上传到阿里云
String imageType = "default";
UploadImageRequest Imagerequest = new UploadImageRequest("LTAI5t6a4Bwa6d21Pd2NRAqo", "QLPSNZCYpe6EI70adnq8s9GRgvcYqo", imageType);
Imagerequest.setInputStream(input);
UploadImageImpl uploadImage = new UploadImageImpl();
UploadImageResponse Imageresponse = uploadImage.upload(Imagerequest);
if(Imageresponse.isSuccess())
{
String imageURL = Imageresponse.getImageURL();
strings[0]=imageURL;
String imageId = Imageresponse.getImageId();
strings[1]=imageId;
//上传结束后获得图片的url 将图片的Url作为视频的封面图片
//获取文件名
String originalFilename = multipartFile.getOriginalFilename();
UploadStreamResponse response=null;
//上传的视频的标题
String title = originalFilename.substring(0, originalFilename.lastIndexOf("."));
UploadStreamRequest request = new UploadStreamRequest(ConstantPropertiesUtils.KEY_ID, ConstantPropertiesUtils.KEY_SECRET, title, originalFilename, multipartFile.getInputStream());
request.setCoverURL(imageURL);
UploadVideoImpl uploader = new UploadVideoImpl();
response = uploader.uploadStream(request);
String videoId = response.getVideoId();
System.out.println(videoId);
strings[2]=videoId;
}
} catch (IOException e) {
e.printStackTrace();
}
return strings;
}
5.工具类
实现照片旋转
package com.ts.oss.util;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
public class ImageRotation {
public static BufferedImage rotateImage(BufferedImage image, double angle) {
int w = image.getWidth();
int h = image.getHeight();
// 计算旋转后图像的新边界
double sin = Math.sin(Math.toRadians(angle));
double cos = Math.cos(Math.toRadians(angle));
int newW = (int) Math.floor(Math.abs(w * cos) + Math.abs(h * sin));
int newH = (int) Math.floor(Math.abs(h * cos) + Math.abs(w * sin));
// 创建一个新的BufferedImage对象,用于存储旋转后的图像
BufferedImage rotatedImage = new BufferedImage(newW, newH, image.getType());
// 获取Graphics2D对象以进行绘制
Graphics2D g2d = rotatedImage.createGraphics();
// 设置渲染提示
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 旋转图像
AffineTransform at = new AffineTransform();
at.rotate(Math.toRadians(angle), newW / 2, newH / 2);
g2d.setTransform(at);
// 绘制旋转后的图像
g2d.drawImage(image, (newW - w) / 2, (newH - h) / 2, null);
// 释放Graphics2D对象的资源
g2d.dispose();
return rotatedImage;
}
}
从配置文件中获取KEY和ID
package com.ts.oss.util;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ConstantPropertiesUtils implements InitializingBean {
//读取配置文件中的内容
@Value("${aliyun.oss.file.endpoint}")
private String endpoint;
@Value("${aliyun.oss.file.keyid}")
private String keyId;
@Value("${aliyun.oss.file.keysecret}")
private String keySecret;
@Value("${aliyun.oss.file.bucketname}")
private String bucketName;
//定义公开静态常量 供其他方法使用
public static String END_POINT;
public static String KEY_ID;
public static String KEY_SECRET;
public static String BUCKETNAME;
@Override
public void afterPropertiesSet() throws Exception {
END_POINT=endpoint;
KEY_ID=keyId;
KEY_SECRET=keySecret;
BUCKETNAME=bucketName;
}
}
在上述Android代码的留白部分加入Okhttp请求
File file=new File(getExternalCacheDir()+"/demo.mp4");
OkHttpClient client = new OkHttpClient();
String sendUrl="http://你的IP地址:你的端口号"+你的Controller请求路径;
MultipartBody.Builder requestBody=new MultipartBody.Builder().setType(MultipartBody.FORM);
RequestBody fileBody=RequestBody.create(MediaType.parse("video/*"),file);
requestBody.addFormDataPart("file", file.getName(),fileBody);
Request request=new Request.Builder()
.url(sendUrl)
.post(requestBody.build())
.build();//创建http请求
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
String responseBody = response.body().string();
// 在请求成功时调用回调函数
}
});
效果