jiangdongguo
7 years ago
21 changed files with 3353 additions and 1697 deletions
@ -1,198 +0,0 @@ |
|||
/* |
|||
* UVCCamera |
|||
* library and sample to access to UVC web camera on non-rooted Android device |
|||
* |
|||
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
* |
|||
* All files in the folder are under this Apache License, Version 2.0. |
|||
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
|||
* may have a different license, see the respective files. |
|||
*/ |
|||
|
|||
package com.jiangdg.usbcamera.view; |
|||
|
|||
import android.Manifest; |
|||
import android.annotation.SuppressLint; |
|||
import android.app.Activity; |
|||
import android.content.pm.PackageManager; |
|||
import android.os.Bundle; |
|||
import android.os.Handler; |
|||
import android.os.Looper; |
|||
import android.support.annotation.NonNull; |
|||
import android.support.annotation.StringRes; |
|||
import android.support.v7.app.AppCompatActivity; |
|||
import android.util.Log; |
|||
import android.widget.Toast; |
|||
|
|||
import com.serenegiant.dialog.MessageDialogFragment; |
|||
import com.serenegiant.utils.BuildCheck; |
|||
import com.serenegiant.utils.HandlerThreadHandler; |
|||
import com.serenegiant.utils.PermissionCheck; |
|||
|
|||
/** |
|||
* Created by saki on 2016/11/18. |
|||
* |
|||
*/ |
|||
public class BaseActivity extends AppCompatActivity { |
|||
|
|||
private static boolean DEBUG = false; |
|||
private static final String TAG = BaseActivity.class.getSimpleName(); |
|||
|
|||
// 处理UI的Handler
|
|||
private final Handler mUIHandler = new Handler(Looper.getMainLooper()); |
|||
private final Thread mUiThread = mUIHandler.getLooper().getThread(); |
|||
// 工作线程Handler
|
|||
private Handler mWorkerHandler; |
|||
private long mWorkerThreadID = -1; |
|||
|
|||
@Override |
|||
protected void onCreate(final Bundle savedInstanceState) { |
|||
super.onCreate(savedInstanceState); |
|||
// 创建工作线程
|
|||
if (mWorkerHandler == null) { |
|||
mWorkerHandler = HandlerThreadHandler.createHandler(TAG); |
|||
mWorkerThreadID = mWorkerHandler.getLooper().getThread().getId(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected void onPause() { |
|||
clearToast(); |
|||
super.onPause(); |
|||
} |
|||
|
|||
@Override |
|||
protected synchronized void onDestroy() { |
|||
// 释放线程资源
|
|||
if (mWorkerHandler != null) { |
|||
try { |
|||
mWorkerHandler.getLooper().quit(); |
|||
} catch (final Exception e) { |
|||
//
|
|||
} |
|||
mWorkerHandler = null; |
|||
} |
|||
super.onDestroy(); |
|||
} |
|||
|
|||
//================================================================================
|
|||
/** |
|||
* |
|||
* 子线程中更新UI,duration为延迟多久执行 |
|||
* |
|||
*/ |
|||
public final void runOnUiThread(final Runnable task, final long duration) { |
|||
if (task == null) return; |
|||
mUIHandler.removeCallbacks(task); |
|||
if ((duration > 0) || Thread.currentThread() != mUiThread) { |
|||
mUIHandler.postDelayed(task, duration); |
|||
} else { |
|||
try { |
|||
task.run(); |
|||
} catch (final Exception e) { |
|||
Log.w(TAG, e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 移除更新UI task |
|||
* @param task |
|||
*/ |
|||
public final void removeFromUiThread(final Runnable task) { |
|||
if (task == null) return; |
|||
mUIHandler.removeCallbacks(task); |
|||
} |
|||
|
|||
/** |
|||
* 工作子线程中执行的任务 |
|||
*/ |
|||
protected final synchronized void queueEvent(final Runnable task, final long delayMillis) { |
|||
if ((task == null) || (mWorkerHandler == null)) return; |
|||
try { |
|||
mWorkerHandler.removeCallbacks(task); |
|||
if (delayMillis > 0) { |
|||
mWorkerHandler.postDelayed(task, delayMillis); |
|||
} else if (mWorkerThreadID == Thread.currentThread().getId()) { |
|||
task.run(); |
|||
} else { |
|||
mWorkerHandler.post(task); |
|||
} |
|||
} catch (final Exception e) { |
|||
// ignore
|
|||
} |
|||
} |
|||
|
|||
protected final synchronized void removeEvent(final Runnable task) { |
|||
if (task == null) return; |
|||
try { |
|||
mWorkerHandler.removeCallbacks(task); |
|||
} catch (final Exception e) { |
|||
// ignore
|
|||
} |
|||
} |
|||
|
|||
//================================================================================
|
|||
private Toast mToast; |
|||
|
|||
protected void showToast(@StringRes final int msg, final Object... args) { |
|||
removeFromUiThread(mShowToastTask); |
|||
mShowToastTask = new ShowToastTask(msg, args); |
|||
runOnUiThread(mShowToastTask, 0); |
|||
} |
|||
|
|||
|
|||
protected void clearToast() { |
|||
removeFromUiThread(mShowToastTask); |
|||
mShowToastTask = null; |
|||
try { |
|||
if (mToast != null) { |
|||
mToast.cancel(); |
|||
mToast = null; |
|||
} |
|||
} catch (final Exception e) { |
|||
// ignore
|
|||
} |
|||
} |
|||
|
|||
private ShowToastTask mShowToastTask; |
|||
private final class ShowToastTask implements Runnable { |
|||
final int msg; |
|||
final Object args; |
|||
private ShowToastTask(@StringRes final int msg, final Object... args) { |
|||
this.msg = msg; |
|||
this.args = args; |
|||
} |
|||
|
|||
@Override |
|||
public void run() { |
|||
try { |
|||
if (mToast != null) { |
|||
mToast.cancel(); |
|||
mToast = null; |
|||
} |
|||
if (args != null) { |
|||
final String _msg = getString(msg, args); |
|||
mToast = Toast.makeText(BaseActivity.this, _msg, Toast.LENGTH_SHORT); |
|||
} else { |
|||
mToast = Toast.makeText(BaseActivity.this, msg, Toast.LENGTH_SHORT); |
|||
} |
|||
mToast.show(); |
|||
} catch (final Exception e) { |
|||
// ignore
|
|||
} |
|||
} |
|||
} |
|||
} |
@ -1,250 +0,0 @@ |
|||
package com.jiangdg.usbcamera.view; |
|||
|
|||
import android.hardware.usb.UsbDevice; |
|||
import android.os.Bundle; |
|||
import android.util.Log; |
|||
import android.view.Surface; |
|||
import android.view.SurfaceHolder; |
|||
import android.view.SurfaceView; |
|||
import android.view.View; |
|||
import android.view.View.OnClickListener; |
|||
import android.widget.ImageButton; |
|||
import android.widget.Toast; |
|||
|
|||
import com.jiangdg.usbcamera.R; |
|||
import com.serenegiant.usb.CameraDialog; |
|||
import com.serenegiant.usb.USBMonitor; |
|||
import com.serenegiant.usb.USBMonitor.OnDeviceConnectListener; |
|||
import com.serenegiant.usb.USBMonitor.UsbControlBlock; |
|||
import com.serenegiant.usb.UVCCamera; |
|||
|
|||
import butterknife.BindView; |
|||
import butterknife.ButterKnife; |
|||
import butterknife.OnClick; |
|||
|
|||
public class MainActivity extends BaseActivity implements CameraDialog.CameraDialogParent { |
|||
private static final boolean DEBUG = true; |
|||
private static final String TAG = "MainActivity"; |
|||
@BindView(R.id.camera_surface_view) |
|||
public SurfaceView mUVCCameraView; |
|||
|
|||
private final Object mSync = new Object(); |
|||
// USB和USB Camera访问管理类
|
|||
private USBMonitor mUSBMonitor; |
|||
private UVCCamera mUVCCamera; |
|||
|
|||
private Surface mPreviewSurface; |
|||
private boolean isActive, isPreview; |
|||
|
|||
private final SurfaceHolder.Callback mSurfaceViewCallback = new SurfaceHolder.Callback() { |
|||
@Override |
|||
public void surfaceCreated(final SurfaceHolder holder) { |
|||
if (DEBUG) Log.v(TAG, "surfaceCreated:"); |
|||
} |
|||
|
|||
@Override |
|||
public void surfaceChanged(final SurfaceHolder holder, final int format, final int width, final int height) { |
|||
if ((width == 0) || (height == 0)) return; |
|||
if (DEBUG) Log.v(TAG, "surfaceChanged:"); |
|||
mPreviewSurface = holder.getSurface(); |
|||
synchronized (mSync) { |
|||
if (isActive && !isPreview && (mUVCCamera != null)) { |
|||
mUVCCamera.setPreviewDisplay(mPreviewSurface); |
|||
mUVCCamera.startPreview(); |
|||
isPreview = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void surfaceDestroyed(final SurfaceHolder holder) { |
|||
if (DEBUG) Log.v(TAG, "surfaceDestroyed:"); |
|||
synchronized (mSync) { |
|||
if (mUVCCamera != null) { |
|||
mUVCCamera.stopPreview(); |
|||
} |
|||
isPreview = false; |
|||
} |
|||
mPreviewSurface = null; |
|||
} |
|||
}; |
|||
|
|||
@Override |
|||
protected void onCreate(final Bundle savedInstanceState) { |
|||
super.onCreate(savedInstanceState); |
|||
setContentView(R.layout.activity_main); |
|||
// 绑定Activity
|
|||
ButterKnife.bind(this); |
|||
mUVCCameraView.getHolder().addCallback(mSurfaceViewCallback); |
|||
|
|||
// 初始化USBMonitor
|
|||
// 注册USB设备监听器
|
|||
mUSBMonitor = new USBMonitor(this, new OnDeviceConnectListener() { |
|||
@Override |
|||
public void onAttach(final UsbDevice device) { |
|||
if (DEBUG) Log.v(TAG, "onAttach:"); |
|||
Toast.makeText(MainActivity.this, "检测到USB设备", Toast.LENGTH_SHORT).show(); |
|||
} |
|||
|
|||
@Override |
|||
public void onConnect(final UsbDevice device, final UsbControlBlock ctrlBlock, final boolean createNew) { |
|||
if (DEBUG) Log.v(TAG, "onConnect:"); |
|||
Toast.makeText(MainActivity.this, "成功连接到USB设备", Toast.LENGTH_SHORT).show(); |
|||
synchronized (mSync) { |
|||
if (mUVCCamera != null) { |
|||
mUVCCamera.destroy(); |
|||
} |
|||
isActive = isPreview = false; |
|||
} |
|||
queueEvent(new Runnable() { |
|||
@Override |
|||
public void run() { |
|||
synchronized (mSync) { |
|||
final UVCCamera camera = new UVCCamera(); |
|||
camera.open(ctrlBlock); |
|||
if (DEBUG) Log.i(TAG, "supportedSize:" + camera.getSupportedSize()); |
|||
try { |
|||
camera.setPreviewSize(UVCCamera.DEFAULT_PREVIEW_WIDTH, UVCCamera.DEFAULT_PREVIEW_HEIGHT, UVCCamera.FRAME_FORMAT_MJPEG); |
|||
} catch (final IllegalArgumentException e) { |
|||
try { |
|||
// fallback to YUV mode
|
|||
camera.setPreviewSize(UVCCamera.DEFAULT_PREVIEW_WIDTH, UVCCamera.DEFAULT_PREVIEW_HEIGHT, UVCCamera.DEFAULT_PREVIEW_MODE); |
|||
} catch (final IllegalArgumentException e1) { |
|||
camera.destroy(); |
|||
return; |
|||
} |
|||
} |
|||
mPreviewSurface = mUVCCameraView.getHolder().getSurface(); |
|||
if (mPreviewSurface != null) { |
|||
isActive = true; |
|||
camera.setPreviewDisplay(mPreviewSurface); |
|||
camera.startPreview(); |
|||
isPreview = true; |
|||
} |
|||
synchronized (mSync) { |
|||
mUVCCamera = camera; |
|||
} |
|||
} |
|||
} |
|||
}, 0); |
|||
} |
|||
|
|||
@Override |
|||
public void onDisconnect(final UsbDevice device, final UsbControlBlock ctrlBlock) { |
|||
if (DEBUG) Log.v(TAG, "onDisconnect:"); |
|||
Toast.makeText(MainActivity.this, "与USB设备断开连接", Toast.LENGTH_SHORT).show(); |
|||
// XXX you should check whether the comming device equal to camera device that currently using
|
|||
queueEvent(new Runnable() { |
|||
@Override |
|||
public void run() { |
|||
synchronized (mSync) { |
|||
if (mUVCCamera != null) { |
|||
mUVCCamera.close(); |
|||
if (mPreviewSurface != null) { |
|||
mPreviewSurface.release(); |
|||
mPreviewSurface = null; |
|||
} |
|||
isActive = isPreview = false; |
|||
} |
|||
} |
|||
} |
|||
}, 0); |
|||
} |
|||
|
|||
@Override |
|||
public void onDettach(final UsbDevice device) { |
|||
if (DEBUG) Log.v(TAG, "onDettach:"); |
|||
Toast.makeText(MainActivity.this, "未检测到USB设备", Toast.LENGTH_SHORT).show(); |
|||
} |
|||
|
|||
@Override |
|||
public void onCancel(final UsbDevice device) { |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
protected void onStart() { |
|||
super.onStart(); |
|||
if (DEBUG) Log.v(TAG, "onStart:"); |
|||
synchronized (mSync) { |
|||
// 注册
|
|||
if (mUSBMonitor != null) { |
|||
mUSBMonitor.register(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected void onStop() { |
|||
if (DEBUG) Log.v(TAG, "onStop:"); |
|||
synchronized (mSync) { |
|||
// 注销
|
|||
if (mUSBMonitor != null) { |
|||
mUSBMonitor.unregister(); |
|||
} |
|||
} |
|||
super.onStop(); |
|||
} |
|||
|
|||
@Override |
|||
protected void onDestroy() { |
|||
if (DEBUG) Log.v(TAG, "onDestroy:"); |
|||
synchronized (mSync) { |
|||
isActive = isPreview = false; |
|||
if (mUVCCamera != null) { |
|||
mUVCCamera.destroy(); |
|||
mUVCCamera = null; |
|||
} |
|||
// 释放资源
|
|||
if (mUSBMonitor != null) { |
|||
mUSBMonitor.destroy(); |
|||
mUSBMonitor = null; |
|||
} |
|||
} |
|||
mUVCCameraView = null; |
|||
super.onDestroy(); |
|||
} |
|||
|
|||
@OnClick({R.id.camera_surface_view}) |
|||
public void onViewClicked(View view){ |
|||
int vId= view.getId(); |
|||
switch (vId){ |
|||
case R.id.camera_surface_view: |
|||
if (mUVCCamera == null) { |
|||
// XXX calling CameraDialog.showDialog is necessary at only first time(only when app has no permission).
|
|||
// 当APP访问USB设备没有被授权时,弹出对话框
|
|||
CameraDialog.showDialog(MainActivity.this); |
|||
} else { |
|||
synchronized (mSync) { |
|||
mUVCCamera.destroy(); |
|||
mUVCCamera = null; |
|||
isActive = isPreview = false; |
|||
} |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* to access from CameraDialog |
|||
* @return |
|||
*/ |
|||
@Override |
|||
public USBMonitor getUSBMonitor() { |
|||
return mUSBMonitor; |
|||
} |
|||
|
|||
@Override |
|||
public void onDialogResult(boolean canceled) { |
|||
if (canceled) { |
|||
runOnUiThread(new Runnable() { |
|||
@Override |
|||
public void run() { |
|||
// FIXME
|
|||
} |
|||
}, 0); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,169 @@ |
|||
package com.jiangdg.usbcamera.view; |
|||
|
|||
import android.hardware.usb.UsbDevice; |
|||
import android.os.Bundle; |
|||
import android.support.annotation.Nullable; |
|||
import android.support.v7.app.AppCompatActivity; |
|||
import android.view.View; |
|||
import android.widget.Button; |
|||
import android.widget.Toast; |
|||
|
|||
import com.jiangdg.usbcamera.R; |
|||
import com.jiangdg.usbcamera.USBCameraManager; |
|||
import com.serenegiant.usb.CameraDialog; |
|||
import com.serenegiant.usb.USBMonitor; |
|||
import com.serenegiant.usb.widget.CameraViewInterface; |
|||
|
|||
import butterknife.BindView; |
|||
import butterknife.ButterKnife; |
|||
import butterknife.OnClick; |
|||
|
|||
/** |
|||
* AndroidUSBCamera引擎使用Demo |
|||
* |
|||
* Created by jiangdongguo on 2017/9/30. |
|||
*/ |
|||
|
|||
public class USBCameraActivity extends AppCompatActivity implements CameraDialog.CameraDialogParent{ |
|||
@BindView(R.id.camera_view) |
|||
public View mTextureView; |
|||
@BindView(R.id.btn_capture_pic) |
|||
public Button mBtnCapture; |
|||
@BindView(R.id.btn_rec_video) |
|||
public Button mBtnRecord; |
|||
|
|||
private USBCameraManager mUSBManager; |
|||
private CameraViewInterface mUVCCameraView; |
|||
|
|||
// USB设备监听器
|
|||
private USBCameraManager.OnMyDevConnectListener listener = new USBCameraManager.OnMyDevConnectListener() { |
|||
@Override |
|||
public void onAttachDev(UsbDevice device) { |
|||
showShortMsg("检测到设备:"+device.getDeviceName()); |
|||
} |
|||
|
|||
@Override |
|||
public void onDettachDev(UsbDevice device) { |
|||
showShortMsg(device.getDeviceName()+"已拨出"); |
|||
} |
|||
|
|||
@Override |
|||
public void onConnectDev(UsbDevice device) { |
|||
// 处理连接到设备后的逻辑
|
|||
} |
|||
|
|||
@Override |
|||
public void onDisConnectDev(UsbDevice device) { |
|||
// 处理与设备断开后的逻辑
|
|||
} |
|||
}; |
|||
|
|||
@Override |
|||
protected void onCreate(@Nullable Bundle savedInstanceState) { |
|||
super.onCreate(savedInstanceState); |
|||
setContentView(R.layout.activity_usbcamera); |
|||
ButterKnife.bind(this); |
|||
// 初始化引擎
|
|||
mUSBManager = USBCameraManager.getInstance(); |
|||
mUVCCameraView = (CameraViewInterface) mTextureView; |
|||
mUSBManager.init(this, mUVCCameraView, listener); |
|||
} |
|||
|
|||
@Override |
|||
protected void onStart() { |
|||
super.onStart(); |
|||
// 注册USB事件广播监听器
|
|||
if(mUSBManager != null){ |
|||
mUSBManager.registerUSB(); |
|||
} |
|||
// 恢复Camera预览
|
|||
if(mUVCCameraView != null){ |
|||
mUVCCameraView.onResume(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected void onStop() { |
|||
super.onStop(); |
|||
// 注销USB事件广播监听器
|
|||
if(mUSBManager != null){ |
|||
mUSBManager.unregisterUSB(); |
|||
} |
|||
// 暂停Camera预览
|
|||
if(mUVCCameraView != null){ |
|||
mUVCCameraView.onPause(); |
|||
} |
|||
} |
|||
|
|||
@OnClick({R.id.camera_view, R.id.btn_capture_pic, R.id.btn_rec_video}) |
|||
public void onViewClick(View view) { |
|||
int vId = view.getId(); |
|||
switch (vId) { |
|||
// 开启或关闭Camera
|
|||
case R.id.camera_view: |
|||
if(mUSBManager != null){ |
|||
boolean isOpened = mUSBManager.isCameraOpened(); |
|||
if(! isOpened){ |
|||
CameraDialog.showDialog(USBCameraActivity.this); |
|||
}else { |
|||
mUSBManager.closeCamera(); |
|||
} |
|||
} |
|||
|
|||
break; |
|||
case R.id.btn_capture_pic: |
|||
if(mUSBManager == null || ! mUSBManager.isCameraOpened()){ |
|||
showShortMsg("抓拍异常,摄像头未开启"); |
|||
return; |
|||
} |
|||
String picPath = USBCameraManager.ROOT_PATH+System.currentTimeMillis() |
|||
+USBCameraManager.SUFFIX_PNG; |
|||
mUSBManager.capturePicture(picPath); |
|||
|
|||
showShortMsg("保存路径:"+picPath); |
|||
break; |
|||
case R.id.btn_rec_video: |
|||
if(mUSBManager == null || ! mUSBManager.isCameraOpened()){ |
|||
showShortMsg("录制异常,摄像头未开启"); |
|||
return; |
|||
} |
|||
if(! mUSBManager.isRecording()){ |
|||
String videoPath = USBCameraManager.ROOT_PATH+System.currentTimeMillis() |
|||
+USBCameraManager.SUFFIX_MP4; |
|||
mUSBManager.startRecording(videoPath); |
|||
|
|||
mBtnRecord.setText("正在录制"); |
|||
} else { |
|||
mUSBManager.stopRecording(); |
|||
|
|||
mBtnRecord.setText("开始录制"); |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected void onDestroy() { |
|||
super.onDestroy(); |
|||
// 释放资源
|
|||
if(mUSBManager != null){ |
|||
mUSBManager.release(); |
|||
} |
|||
} |
|||
|
|||
private void showShortMsg(String msg) { |
|||
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); |
|||
} |
|||
|
|||
@Override |
|||
public USBMonitor getUSBMonitor() { |
|||
return mUSBManager.getUSBMonitor(); |
|||
} |
|||
|
|||
@Override |
|||
public void onDialogResult(boolean canceled) { |
|||
if(canceled){ |
|||
showShortMsg("取消操作"); |
|||
} |
|||
} |
|||
} |
@ -1,15 +0,0 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
|
|||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:id="@+id/container" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
tools:ignore="MergeRootFrame" > |
|||
|
|||
<SurfaceView |
|||
android:id="@+id/camera_surface_view" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" /> |
|||
|
|||
</FrameLayout> |
@ -0,0 +1,39 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:background="#ff000000" |
|||
tools:context=".view.USBCameraActivity" |
|||
tools:ignore="MergeRootFrame"> |
|||
|
|||
<com.serenegiant.usb.widget.UVCCameraTextureView |
|||
android:id="@+id/camera_view" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
android:layout_centerVertical="true" |
|||
android:layout_centerHorizontal="true"/> |
|||
|
|||
<Button |
|||
android:id="@+id/btn_capture_pic" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:layout_alignParentBottom="true" |
|||
android:layout_marginBottom="10dp" |
|||
android:layout_marginRight="10dp" |
|||
android:layout_marginLeft="10dp" |
|||
android:textSize="16sp" |
|||
android:text="抓拍"/> |
|||
|
|||
<Button |
|||
android:layout_above="@id/btn_capture_pic" |
|||
android:id="@+id/btn_rec_video" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginRight="10dp" |
|||
android:layout_marginLeft="10dp" |
|||
android:textSize="16sp" |
|||
android:text="开始录制"/> |
|||
|
|||
|
|||
</RelativeLayout> |
@ -0,0 +1,207 @@ |
|||
package com.jiangdg.usbcamera; |
|||
|
|||
import android.app.Activity; |
|||
import android.graphics.SurfaceTexture; |
|||
import android.hardware.usb.UsbDevice; |
|||
import android.os.Environment; |
|||
|
|||
import com.serenegiant.usb.CameraDialog; |
|||
import com.serenegiant.usb.USBMonitor; |
|||
import com.serenegiant.usb.common.UVCCameraHandler; |
|||
import com.serenegiant.usb.widget.CameraViewInterface; |
|||
|
|||
import java.io.File; |
|||
|
|||
/**USB摄像头工具类 |
|||
* |
|||
* Created by jiangdongguo on 2017/9/30. |
|||
*/ |
|||
|
|||
public class USBCameraManager{ |
|||
public static final String ROOT_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() |
|||
+ File.separator; |
|||
public static final String SUFFIX_PNG = ".png"; |
|||
public static final String SUFFIX_MP4 = ".mp4"; |
|||
private static final String TAG = "USBCameraManager"; |
|||
private static final int PREVIEW_WIDTH = 640; |
|||
private static final int PREVIEW_HEIGHT = 480; |
|||
private static final int ENCODER_TYPE = 1; |
|||
//0为YUYV,1为MJPEG
|
|||
private static final int PREVIEW_FORMAT = 1; |
|||
|
|||
private static USBCameraManager mUsbCamManager; |
|||
// USB设备管理类
|
|||
private USBMonitor mUSBMonitor; |
|||
// Camera业务逻辑处理
|
|||
private UVCCameraHandler mCameraHandler; |
|||
|
|||
private USBCameraManager(){} |
|||
|
|||
public static USBCameraManager getInstance(){ |
|||
if(mUsbCamManager == null){ |
|||
mUsbCamManager = new USBCameraManager(); |
|||
} |
|||
return mUsbCamManager; |
|||
} |
|||
|
|||
public interface OnMyDevConnectListener{ |
|||
void onAttachDev(UsbDevice device); |
|||
void onDettachDev(UsbDevice device); |
|||
void onConnectDev(UsbDevice device); |
|||
void onDisConnectDev(UsbDevice device); |
|||
} |
|||
|
|||
/** 初始化 |
|||
* |
|||
* context 上下文 |
|||
* cameraView Camera要渲染的Surface |
|||
* listener USB设备检测与连接状态事件监听器 |
|||
* */ |
|||
public void init(Activity activity, final CameraViewInterface cameraView, final OnMyDevConnectListener listener){ |
|||
if(cameraView == null) |
|||
throw new NullPointerException("CameraViewInterface cannot be null!"); |
|||
mUSBMonitor = new USBMonitor(activity.getApplicationContext(), new USBMonitor.OnDeviceConnectListener() { |
|||
// 当检测到USB设备,被回调
|
|||
@Override |
|||
public void onAttach(UsbDevice device) { |
|||
if(listener != null){ |
|||
listener.onAttachDev(device); |
|||
} |
|||
} |
|||
|
|||
// 当拨出或未检测到USB设备,被回调
|
|||
@Override |
|||
public void onDettach(UsbDevice device) { |
|||
if(listener != null){ |
|||
listener.onDettachDev(device); |
|||
} |
|||
} |
|||
|
|||
// 当连接到USB Camera时,被回调
|
|||
@Override |
|||
public void onConnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock, boolean createNew) { |
|||
if(listener != null){ |
|||
listener.onConnectDev(device); |
|||
} |
|||
// 打开摄像头
|
|||
openCamera(ctrlBlock); |
|||
// 开启预览
|
|||
startPreview(cameraView); |
|||
} |
|||
|
|||
// 当与USB Camera断开连接时,被回调
|
|||
@Override |
|||
public void onDisconnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock) { |
|||
if(listener != null){ |
|||
listener.onDisConnectDev(device); |
|||
} |
|||
// 关闭摄像头
|
|||
closeCamera(); |
|||
} |
|||
|
|||
@Override |
|||
public void onCancel(UsbDevice device) { |
|||
} |
|||
}); |
|||
|
|||
// 设置长宽比
|
|||
cameraView.setAspectRatio(PREVIEW_WIDTH / (float)PREVIEW_HEIGHT); |
|||
mCameraHandler = UVCCameraHandler.createHandler(activity,cameraView,ENCODER_TYPE, |
|||
PREVIEW_WIDTH,PREVIEW_HEIGHT,PREVIEW_FORMAT); |
|||
} |
|||
|
|||
/** |
|||
* 注册检测USB设备广播接收器 |
|||
* */ |
|||
public void registerUSB(){ |
|||
if(mUSBMonitor != null){ |
|||
mUSBMonitor.register(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 注销检测USB设备广播接收器 |
|||
*/ |
|||
public void unregisterUSB(){ |
|||
if(mUSBMonitor != null){ |
|||
mUSBMonitor.unregister(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 抓拍照片 |
|||
* */ |
|||
public void capturePicture(String savePath){ |
|||
if(mCameraHandler != null && mCameraHandler.isOpened()){ |
|||
mCameraHandler.captureStill(savePath); |
|||
} |
|||
} |
|||
|
|||
public void startRecording(String videoPath){ |
|||
if(mCameraHandler != null && ! isRecording()){ |
|||
mCameraHandler.startRecording(videoPath); |
|||
} |
|||
} |
|||
|
|||
public void stopRecording(){ |
|||
if(mCameraHandler != null && isRecording()){ |
|||
mCameraHandler.stopRecording(); |
|||
} |
|||
} |
|||
|
|||
public boolean isRecording(){ |
|||
if(mCameraHandler != null){ |
|||
return mCameraHandler.isRecording(); |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
public boolean isCameraOpened(){ |
|||
if(mCameraHandler != null){ |
|||
return mCameraHandler.isOpened(); |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
|
|||
|
|||
/** |
|||
* 释放资源 |
|||
* */ |
|||
public void release(){ |
|||
//释放CameraHandler占用的相关资源
|
|||
if(mCameraHandler != null){ |
|||
mCameraHandler.release(); |
|||
mCameraHandler = null; |
|||
} |
|||
// 释放USBMonitor占用的相关资源
|
|||
if(mUSBMonitor != null){ |
|||
mUSBMonitor.destroy(); |
|||
mUSBMonitor = null; |
|||
} |
|||
} |
|||
|
|||
public USBMonitor getUSBMonitor() { |
|||
return mUSBMonitor; |
|||
} |
|||
|
|||
|
|||
public void closeCamera() { |
|||
if(mCameraHandler != null){ |
|||
mCameraHandler.close(); |
|||
} |
|||
} |
|||
|
|||
private void openCamera(USBMonitor.UsbControlBlock ctrlBlock) { |
|||
if(mCameraHandler != null){ |
|||
mCameraHandler.open(ctrlBlock); |
|||
} |
|||
} |
|||
|
|||
private void startPreview(CameraViewInterface cameraView) { |
|||
SurfaceTexture st = cameraView.getSurfaceTexture(); |
|||
if(mCameraHandler != null){ |
|||
mCameraHandler.startPreview(st); |
|||
} |
|||
} |
|||
} |
File diff suppressed because it is too large
@ -0,0 +1,27 @@ |
|||
/* |
|||
* UVCCamera |
|||
* library and sample to access to UVC web camera on non-rooted Android device |
|||
* |
|||
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
* |
|||
* All files in the folder are under this Apache License, Version 2.0. |
|||
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
|||
* may have a different license, see the respective files. |
|||
*/ |
|||
|
|||
package com.serenegiant.usb.encoder; |
|||
|
|||
public interface IAudioEncoder { |
|||
} |
@ -0,0 +1,28 @@ |
|||
/* |
|||
* UVCCamera |
|||
* library and sample to access to UVC web camera on non-rooted Android device |
|||
* |
|||
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
* |
|||
* All files in the folder are under this Apache License, Version 2.0. |
|||
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
|||
* may have a different license, see the respective files. |
|||
*/ |
|||
|
|||
package com.serenegiant.usb.encoder; |
|||
|
|||
public interface IVideoEncoder { |
|||
public boolean frameAvailableSoon(); |
|||
} |
@ -0,0 +1,233 @@ |
|||
/* |
|||
* UVCCamera |
|||
* library and sample to access to UVC web camera on non-rooted Android device |
|||
* |
|||
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
* |
|||
* All files in the folder are under this Apache License, Version 2.0. |
|||
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
|||
* may have a different license, see the respective files. |
|||
*/ |
|||
|
|||
package com.serenegiant.usb.encoder; |
|||
|
|||
import android.media.AudioFormat; |
|||
import android.media.AudioRecord; |
|||
import android.media.MediaCodec; |
|||
import android.media.MediaCodecInfo; |
|||
import android.media.MediaCodecList; |
|||
import android.media.MediaFormat; |
|||
import android.media.MediaRecorder; |
|||
import android.util.Log; |
|||
|
|||
import java.io.IOException; |
|||
import java.nio.ByteBuffer; |
|||
import java.nio.ByteOrder; |
|||
|
|||
public class MediaAudioEncoder extends MediaEncoder implements IAudioEncoder { |
|||
private static final boolean DEBUG = true; // TODO set false on release
|
|||
private static final String TAG = "MediaAudioEncoder"; |
|||
|
|||
private static final String MIME_TYPE = "audio/mp4a-latm"; |
|||
private static final int SAMPLE_RATE = 44100; // 44.1[KHz] is only setting guaranteed to be available on all devices.
|
|||
private static final int BIT_RATE = 64000; |
|||
public static final int SAMPLES_PER_FRAME = 1024; // AAC, bytes/frame/channel
|
|||
public static final int FRAMES_PER_BUFFER = 25; // AAC, frame/buffer/sec
|
|||
|
|||
private AudioThread mAudioThread = null; |
|||
|
|||
public MediaAudioEncoder(final MediaMuxerWrapper muxer, final MediaEncoderListener listener) { |
|||
super(muxer, listener); |
|||
} |
|||
|
|||
@Override |
|||
protected void prepare() throws IOException { |
|||
if (DEBUG) Log.v(TAG, "prepare:"); |
|||
mTrackIndex = -1; |
|||
mMuxerStarted = mIsEOS = false; |
|||
// prepare MediaCodec for AAC encoding of audio data from inernal mic.
|
|||
final MediaCodecInfo audioCodecInfo = selectAudioCodec(MIME_TYPE); |
|||
if (audioCodecInfo == null) { |
|||
Log.e(TAG, "Unable to find an appropriate codec for " + MIME_TYPE); |
|||
return; |
|||
} |
|||
if (DEBUG) Log.i(TAG, "selected codec: " + audioCodecInfo.getName()); |
|||
|
|||
final MediaFormat audioFormat = MediaFormat.createAudioFormat(MIME_TYPE, SAMPLE_RATE, 1); |
|||
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); |
|||
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, AudioFormat.CHANNEL_IN_MONO); |
|||
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); |
|||
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); |
|||
// audioFormat.setLong(MediaFormat.KEY_MAX_INPUT_SIZE, inputFile.length());
|
|||
// audioFormat.setLong(MediaFormat.KEY_DURATION, (long)durationInMs );
|
|||
if (DEBUG) Log.i(TAG, "format: " + audioFormat); |
|||
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); |
|||
mMediaCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); |
|||
mMediaCodec.start(); |
|||
if (DEBUG) Log.i(TAG, "prepare finishing"); |
|||
if (mListener != null) { |
|||
try { |
|||
mListener.onPrepared(this); |
|||
} catch (final Exception e) { |
|||
Log.e(TAG, "prepare:", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected void startRecording() { |
|||
super.startRecording(); |
|||
// create and execute audio capturing thread using internal mic
|
|||
if (mAudioThread == null) { |
|||
mAudioThread = new AudioThread(); |
|||
mAudioThread.start(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected void release() { |
|||
mAudioThread = null; |
|||
super.release(); |
|||
} |
|||
|
|||
private static final int[] AUDIO_SOURCES = new int[] { |
|||
MediaRecorder.AudioSource.DEFAULT, |
|||
MediaRecorder.AudioSource.MIC, |
|||
MediaRecorder.AudioSource.CAMCORDER, |
|||
}; |
|||
|
|||
/** |
|||
* Thread to capture audio data from internal mic as uncompressed 16bit PCM data |
|||
* and write them to the MediaCodec encoder |
|||
*/ |
|||
private class AudioThread extends Thread { |
|||
@Override |
|||
public void run() { |
|||
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO); // THREAD_PRIORITY_URGENT_AUDIO
|
|||
int cnt = 0; |
|||
final int min_buffer_size = AudioRecord.getMinBufferSize( |
|||
SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); |
|||
int buffer_size = SAMPLES_PER_FRAME * FRAMES_PER_BUFFER; |
|||
if (buffer_size < min_buffer_size) |
|||
buffer_size = ((min_buffer_size / SAMPLES_PER_FRAME) + 1) * SAMPLES_PER_FRAME * 2; |
|||
final ByteBuffer buf = ByteBuffer.allocateDirect(SAMPLES_PER_FRAME).order(ByteOrder.nativeOrder()); |
|||
AudioRecord audioRecord = null; |
|||
for (final int src: AUDIO_SOURCES) { |
|||
try { |
|||
audioRecord = new AudioRecord(src, |
|||
SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, buffer_size); |
|||
if (audioRecord != null) { |
|||
if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { |
|||
audioRecord.release(); |
|||
audioRecord = null; |
|||
} |
|||
} |
|||
} catch (final Exception e) { |
|||
audioRecord = null; |
|||
} |
|||
if (audioRecord != null) { |
|||
break; |
|||
} |
|||
} |
|||
if (audioRecord != null) { |
|||
try { |
|||
if (mIsCapturing) { |
|||
if (DEBUG) Log.v(TAG, "AudioThread:start audio recording"); |
|||
int readBytes; |
|||
audioRecord.startRecording(); |
|||
try { |
|||
for ( ; mIsCapturing && !mRequestStop && !mIsEOS ; ) { |
|||
// read audio data from internal mic
|
|||
buf.clear(); |
|||
try { |
|||
readBytes = audioRecord.read(buf, SAMPLES_PER_FRAME); |
|||
} catch (final Exception e) { |
|||
break; |
|||
} |
|||
if (readBytes > 0) { |
|||
// set audio data to encoder
|
|||
buf.position(readBytes); |
|||
buf.flip(); |
|||
encode(buf, readBytes, getPTSUs()); |
|||
frameAvailableSoon(); |
|||
cnt++; |
|||
} |
|||
} |
|||
if (cnt > 0) { |
|||
frameAvailableSoon(); |
|||
} |
|||
} finally { |
|||
audioRecord.stop(); |
|||
} |
|||
} |
|||
} catch (final Exception e) { |
|||
Log.e(TAG, "AudioThread#run", e); |
|||
} finally { |
|||
audioRecord.release(); |
|||
} |
|||
} |
|||
if (cnt == 0) { |
|||
for (int i = 0; mIsCapturing && (i < 5); i++) { |
|||
buf.position(SAMPLES_PER_FRAME); |
|||
buf.flip(); |
|||
try { |
|||
encode(buf, SAMPLES_PER_FRAME, getPTSUs()); |
|||
frameAvailableSoon(); |
|||
} catch (final Exception e) { |
|||
break; |
|||
} |
|||
synchronized(this) { |
|||
try { |
|||
wait(50); |
|||
} catch (final InterruptedException e) { |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (DEBUG) Log.v(TAG, "AudioThread:finished"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* select the first codec that match a specific MIME type |
|||
* @param mimeType |
|||
* @return |
|||
*/ |
|||
private static final MediaCodecInfo selectAudioCodec(final String mimeType) { |
|||
if (DEBUG) Log.v(TAG, "selectAudioCodec:"); |
|||
|
|||
MediaCodecInfo result = null; |
|||
// get the list of available codecs
|
|||
final int numCodecs = MediaCodecList.getCodecCount(); |
|||
LOOP: for (int i = 0; i < numCodecs; i++) { |
|||
final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); |
|||
if (!codecInfo.isEncoder()) { // skipp decoder
|
|||
continue; |
|||
} |
|||
final String[] types = codecInfo.getSupportedTypes(); |
|||
for (int j = 0; j < types.length; j++) { |
|||
if (DEBUG) Log.i(TAG, "supportedType:" + codecInfo.getName() + ",MIME=" + types[j]); |
|||
if (types[j].equalsIgnoreCase(mimeType)) { |
|||
if (result == null) { |
|||
result = codecInfo; |
|||
break LOOP; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,448 @@ |
|||
/* |
|||
* UVCCamera |
|||
* library and sample to access to UVC web camera on non-rooted Android device |
|||
* |
|||
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
* |
|||
* All files in the folder are under this Apache License, Version 2.0. |
|||
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
|||
* may have a different license, see the respective files. |
|||
*/ |
|||
|
|||
package com.serenegiant.usb.encoder; |
|||
|
|||
import android.media.MediaCodec; |
|||
import android.media.MediaFormat; |
|||
import android.util.Log; |
|||
|
|||
import java.io.IOException; |
|||
import java.lang.ref.WeakReference; |
|||
import java.nio.ByteBuffer; |
|||
|
|||
public abstract class MediaEncoder implements Runnable { |
|||
private static final boolean DEBUG = true; // TODO set false on release
|
|||
private static final String TAG = "MediaEncoder"; |
|||
|
|||
protected static final int TIMEOUT_USEC = 10000; // 10[msec]
|
|||
protected static final int MSG_FRAME_AVAILABLE = 1; |
|||
protected static final int MSG_STOP_RECORDING = 9; |
|||
|
|||
public interface MediaEncoderListener { |
|||
public void onPrepared(MediaEncoder encoder); |
|||
public void onStopped(MediaEncoder encoder); |
|||
} |
|||
|
|||
protected final Object mSync = new Object(); |
|||
/** |
|||
* Flag that indicate this encoder is capturing now. |
|||
*/ |
|||
protected volatile boolean mIsCapturing; |
|||
/** |
|||
* Flag that indicate the frame data will be available soon. |
|||
*/ |
|||
private int mRequestDrain; |
|||
/** |
|||
* Flag to request stop capturing |
|||
*/ |
|||
protected volatile boolean mRequestStop; |
|||
/** |
|||
* Flag that indicate encoder received EOS(End Of Stream) |
|||
*/ |
|||
protected boolean mIsEOS; |
|||
/** |
|||
* Flag the indicate the muxer is running |
|||
*/ |
|||
protected boolean mMuxerStarted; |
|||
/** |
|||
* Track Number |
|||
*/ |
|||
protected int mTrackIndex; |
|||
/** |
|||
* MediaCodec instance for encoding |
|||
*/ |
|||
protected MediaCodec mMediaCodec; // API >= 16(Android4.1.2)
|
|||
/** |
|||
* Weak refarence of MediaMuxerWarapper instance |
|||
*/ |
|||
protected final WeakReference<MediaMuxerWrapper> mWeakMuxer; |
|||
/** |
|||
* BufferInfo instance for dequeuing |
|||
*/ |
|||
private MediaCodec.BufferInfo mBufferInfo; // API >= 16(Android4.1.2)
|
|||
|
|||
protected final MediaEncoderListener mListener; |
|||
|
|||
public MediaEncoder(final MediaMuxerWrapper muxer, final MediaEncoderListener listener) { |
|||
if (listener == null) throw new NullPointerException("MediaEncoderListener is null"); |
|||
if (muxer == null) throw new NullPointerException("MediaMuxerWrapper is null"); |
|||
mWeakMuxer = new WeakReference<MediaMuxerWrapper>(muxer); |
|||
muxer.addEncoder(this); |
|||
mListener = listener; |
|||
synchronized (mSync) { |
|||
// create BufferInfo here for effectiveness(to reduce GC)
|
|||
mBufferInfo = new MediaCodec.BufferInfo(); |
|||
// wait for starting thread
|
|||
new Thread(this, getClass().getSimpleName()).start(); |
|||
try { |
|||
mSync.wait(); |
|||
} catch (final InterruptedException e) { |
|||
} |
|||
} |
|||
} |
|||
|
|||
public String getOutputPath() { |
|||
final MediaMuxerWrapper muxer = mWeakMuxer.get(); |
|||
return muxer != null ? muxer.getOutputPath() : null; |
|||
} |
|||
|
|||
/** |
|||
* the method to indicate frame data is soon available or already available |
|||
* @return return true if encoder is ready to encod. |
|||
*/ |
|||
public boolean frameAvailableSoon() { |
|||
// if (DEBUG) Log.v(TAG, "frameAvailableSoon");
|
|||
synchronized (mSync) { |
|||
if (!mIsCapturing || mRequestStop) { |
|||
return false; |
|||
} |
|||
mRequestDrain++; |
|||
mSync.notifyAll(); |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* encoding loop on private thread |
|||
*/ |
|||
@Override |
|||
public void run() { |
|||
// android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
|
|||
synchronized (mSync) { |
|||
mRequestStop = false; |
|||
mRequestDrain = 0; |
|||
mSync.notify(); |
|||
} |
|||
final boolean isRunning = true; |
|||
boolean localRequestStop; |
|||
boolean localRequestDrain; |
|||
while (isRunning) { |
|||
synchronized (mSync) { |
|||
localRequestStop = mRequestStop; |
|||
localRequestDrain = (mRequestDrain > 0); |
|||
if (localRequestDrain) |
|||
mRequestDrain--; |
|||
} |
|||
if (localRequestStop) { |
|||
drain(); |
|||
// request stop recording
|
|||
signalEndOfInputStream(); |
|||
// process output data again for EOS signale
|
|||
drain(); |
|||
// release all related objects
|
|||
release(); |
|||
break; |
|||
} |
|||
if (localRequestDrain) { |
|||
drain(); |
|||
} else { |
|||
synchronized (mSync) { |
|||
try { |
|||
mSync.wait(); |
|||
} catch (final InterruptedException e) { |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} // end of while
|
|||
if (DEBUG) Log.d(TAG, "Encoder thread exiting"); |
|||
synchronized (mSync) { |
|||
mRequestStop = true; |
|||
mIsCapturing = false; |
|||
} |
|||
} |
|||
|
|||
/* |
|||
* prepareing method for each sub class |
|||
* this method should be implemented in sub class, so set this as abstract method |
|||
* @throws IOException |
|||
*/ |
|||
/*package*/ abstract void prepare() throws IOException; |
|||
|
|||
/*package*/ void startRecording() { |
|||
if (DEBUG) Log.v(TAG, "startRecording"); |
|||
synchronized (mSync) { |
|||
mIsCapturing = true; |
|||
mRequestStop = false; |
|||
mSync.notifyAll(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* the method to request stop encoding |
|||
*/ |
|||
/*package*/ void stopRecording() { |
|||
if (DEBUG) Log.v(TAG, "stopRecording"); |
|||
synchronized (mSync) { |
|||
if (!mIsCapturing || mRequestStop) { |
|||
return; |
|||
} |
|||
mRequestStop = true; // for rejecting newer frame
|
|||
mSync.notifyAll(); |
|||
// We can not know when the encoding and writing finish.
|
|||
// so we return immediately after request to avoid delay of caller thread
|
|||
} |
|||
} |
|||
|
|||
//********************************************************************************
|
|||
//********************************************************************************
|
|||
/** |
|||
* Release all releated objects |
|||
*/ |
|||
protected void release() { |
|||
if (DEBUG) Log.d(TAG, "release:"); |
|||
try { |
|||
mListener.onStopped(this); |
|||
} catch (final Exception e) { |
|||
Log.e(TAG, "failed onStopped", e); |
|||
} |
|||
mIsCapturing = false; |
|||
if (mMediaCodec != null) { |
|||
try { |
|||
mMediaCodec.stop(); |
|||
mMediaCodec.release(); |
|||
mMediaCodec = null; |
|||
} catch (final Exception e) { |
|||
Log.e(TAG, "failed releasing MediaCodec", e); |
|||
} |
|||
} |
|||
if (mMuxerStarted) { |
|||
final MediaMuxerWrapper muxer = mWeakMuxer.get(); |
|||
if (muxer != null) { |
|||
try { |
|||
muxer.stop(); |
|||
} catch (final Exception e) { |
|||
Log.e(TAG, "failed stopping muxer", e); |
|||
} |
|||
} |
|||
} |
|||
mBufferInfo = null; |
|||
} |
|||
|
|||
protected void signalEndOfInputStream() { |
|||
if (DEBUG) Log.d(TAG, "sending EOS to encoder"); |
|||
// signalEndOfInputStream is only avairable for video encoding with surface
|
|||
// and equivalent sending a empty buffer with BUFFER_FLAG_END_OF_STREAM flag.
|
|||
// mMediaCodec.signalEndOfInputStream(); // API >= 18
|
|||
encode((byte[])null, 0, getPTSUs()); |
|||
} |
|||
|
|||
/** |
|||
* Method to set byte array to the MediaCodec encoder |
|||
* @param buffer |
|||
* @param length length of byte array, zero means EOS. |
|||
* @param presentationTimeUs |
|||
*/ |
|||
@SuppressWarnings("deprecation") |
|||
protected void encode(final byte[] buffer, final int length, final long presentationTimeUs) { |
|||
// if (DEBUG) Log.v(TAG, "encode:buffer=" + buffer);
|
|||
if (!mIsCapturing) return; |
|||
int ix = 0, sz; |
|||
final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); |
|||
while (mIsCapturing && ix < length) { |
|||
final int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC); |
|||
if (inputBufferIndex >= 0) { |
|||
final ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; |
|||
inputBuffer.clear(); |
|||
sz = inputBuffer.remaining(); |
|||
sz = (ix + sz < length) ? sz : length - ix; |
|||
if (sz > 0 && (buffer != null)) { |
|||
inputBuffer.put(buffer, ix, sz); |
|||
} |
|||
ix += sz; |
|||
// if (DEBUG) Log.v(TAG, "encode:queueInputBuffer");
|
|||
if (length <= 0) { |
|||
// send EOS
|
|||
mIsEOS = true; |
|||
if (DEBUG) Log.i(TAG, "send BUFFER_FLAG_END_OF_STREAM"); |
|||
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0, |
|||
presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM); |
|||
break; |
|||
} else { |
|||
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, sz, |
|||
presentationTimeUs, 0); |
|||
} |
|||
} else if (inputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { |
|||
// wait for MediaCodec encoder is ready to encode
|
|||
// nothing to do here because MediaCodec#dequeueInputBuffer(TIMEOUT_USEC)
|
|||
// will wait for maximum TIMEOUT_USEC(10msec) on each call
|
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Method to set ByteBuffer to the MediaCodec encoder |
|||
* @param buffer null means EOS |
|||
* @param presentationTimeUs |
|||
*/ |
|||
@SuppressWarnings("deprecation") |
|||
protected void encode(final ByteBuffer buffer, final int length, final long presentationTimeUs) { |
|||
// if (DEBUG) Log.v(TAG, "encode:buffer=" + buffer);
|
|||
if (!mIsCapturing) return; |
|||
int ix = 0, sz; |
|||
final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); |
|||
while (mIsCapturing && ix < length) { |
|||
final int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC); |
|||
if (inputBufferIndex >= 0) { |
|||
final ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; |
|||
inputBuffer.clear(); |
|||
sz = inputBuffer.remaining(); |
|||
sz = (ix + sz < length) ? sz : length - ix; |
|||
if (sz > 0 && (buffer != null)) { |
|||
buffer.position(ix + sz); |
|||
buffer.flip(); |
|||
inputBuffer.put(buffer); |
|||
} |
|||
ix += sz; |
|||
// if (DEBUG) Log.v(TAG, "encode:queueInputBuffer");
|
|||
if (length <= 0) { |
|||
// send EOS
|
|||
mIsEOS = true; |
|||
if (DEBUG) Log.i(TAG, "send BUFFER_FLAG_END_OF_STREAM"); |
|||
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0, |
|||
presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM); |
|||
break; |
|||
} else { |
|||
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, sz, |
|||
presentationTimeUs, 0); |
|||
} |
|||
} else if (inputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { |
|||
// wait for MediaCodec encoder is ready to encode
|
|||
// nothing to do here because MediaCodec#dequeueInputBuffer(TIMEOUT_USEC)
|
|||
// will wait for maximum TIMEOUT_USEC(10msec) on each call
|
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* drain encoded data and write them to muxer |
|||
*/ |
|||
@SuppressWarnings("deprecation") |
|||
protected void drain() { |
|||
if (mMediaCodec == null) return; |
|||
ByteBuffer[] encoderOutputBuffers = mMediaCodec.getOutputBuffers(); |
|||
int encoderStatus, count = 0; |
|||
final MediaMuxerWrapper muxer = mWeakMuxer.get(); |
|||
if (muxer == null) { |
|||
// throw new NullPointerException("muxer is unexpectedly null");
|
|||
Log.w(TAG, "muxer is unexpectedly null"); |
|||
return; |
|||
} |
|||
LOOP: while (mIsCapturing) { |
|||
// get encoded data with maximum timeout duration of TIMEOUT_USEC(=10[msec])
|
|||
encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); |
|||
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { |
|||
// wait 5 counts(=TIMEOUT_USEC x 5 = 50msec) until data/EOS come
|
|||
if (!mIsEOS) { |
|||
if (++count > 5) |
|||
break LOOP; // out of while
|
|||
} |
|||
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { |
|||
if (DEBUG) Log.v(TAG, "INFO_OUTPUT_BUFFERS_CHANGED"); |
|||
// this shoud not come when encoding
|
|||
encoderOutputBuffers = mMediaCodec.getOutputBuffers(); |
|||
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { |
|||
if (DEBUG) Log.v(TAG, "INFO_OUTPUT_FORMAT_CHANGED"); |
|||
// this status indicate the output format of codec is changed
|
|||
// this should come only once before actual encoded data
|
|||
// but this status never come on Android4.3 or less
|
|||
// and in that case, you should treat when MediaCodec.BUFFER_FLAG_CODEC_CONFIG come.
|
|||
if (mMuxerStarted) { // second time request is error
|
|||
throw new RuntimeException("format changed twice"); |
|||
} |
|||
// get output format from codec and pass them to muxer
|
|||
// getOutputFormat should be called after INFO_OUTPUT_FORMAT_CHANGED otherwise crash.
|
|||
final MediaFormat format = mMediaCodec.getOutputFormat(); // API >= 16
|
|||
mTrackIndex = muxer.addTrack(format); |
|||
mMuxerStarted = true; |
|||
if (!muxer.start()) { |
|||
// we should wait until muxer is ready
|
|||
synchronized (muxer) { |
|||
while (!muxer.isStarted()) |
|||
try { |
|||
muxer.wait(100); |
|||
} catch (final InterruptedException e) { |
|||
break LOOP; |
|||
} |
|||
} |
|||
} |
|||
} else if (encoderStatus < 0) { |
|||
// unexpected status
|
|||
if (DEBUG) Log.w(TAG, "drain:unexpected result from encoder#dequeueOutputBuffer: " + encoderStatus); |
|||
} else { |
|||
final ByteBuffer encodedData = encoderOutputBuffers[encoderStatus]; |
|||
if (encodedData == null) { |
|||
// this never should come...may be a MediaCodec internal error
|
|||
throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null"); |
|||
} |
|||
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { |
|||
// You shoud set output format to muxer here when you target Android4.3 or less
|
|||
// but MediaCodec#getOutputFormat can not call here(because INFO_OUTPUT_FORMAT_CHANGED don't come yet)
|
|||
// therefor we should expand and prepare output format from buffer data.
|
|||
// This sample is for API>=18(>=Android 4.3), just ignore this flag here
|
|||
if (DEBUG) Log.d(TAG, "drain:BUFFER_FLAG_CODEC_CONFIG"); |
|||
mBufferInfo.size = 0; |
|||
} |
|||
|
|||
if (mBufferInfo.size != 0) { |
|||
// encoded data is ready, clear waiting counter
|
|||
count = 0; |
|||
if (!mMuxerStarted) { |
|||
// muxer is not ready...this will prrograming failure.
|
|||
throw new RuntimeException("drain:muxer hasn't started"); |
|||
} |
|||
// write encoded data to muxer(need to adjust presentationTimeUs.
|
|||
mBufferInfo.presentationTimeUs = getPTSUs(); |
|||
muxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo); |
|||
prevOutputPTSUs = mBufferInfo.presentationTimeUs; |
|||
} |
|||
// return buffer to encoder
|
|||
mMediaCodec.releaseOutputBuffer(encoderStatus, false); |
|||
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { |
|||
// when EOS come.
|
|||
mMuxerStarted = mIsCapturing = false; |
|||
break; // out of while
|
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* previous presentationTimeUs for writing |
|||
*/ |
|||
private long prevOutputPTSUs = 0; |
|||
/** |
|||
* get next encoding presentationTimeUs |
|||
* @return |
|||
*/ |
|||
protected long getPTSUs() { |
|||
long result = System.nanoTime() / 1000L; |
|||
// presentationTimeUs should be monotonic
|
|||
// otherwise muxer fail to write
|
|||
if (result < prevOutputPTSUs) |
|||
result = (prevOutputPTSUs - result) + result; |
|||
return result; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,188 @@ |
|||
package com.serenegiant.usb.encoder; |
|||
|
|||
import android.media.MediaCodec; |
|||
import android.media.MediaFormat; |
|||
import android.media.MediaMuxer; |
|||
import android.os.Environment; |
|||
import android.text.TextUtils; |
|||
import android.util.Log; |
|||
|
|||
import java.io.File; |
|||
import java.io.IOException; |
|||
import java.nio.ByteBuffer; |
|||
import java.text.SimpleDateFormat; |
|||
import java.util.GregorianCalendar; |
|||
import java.util.Locale; |
|||
|
|||
public class MediaMuxerWrapper { |
|||
private static final boolean DEBUG = true; // TODO set false on release
|
|||
private static final String TAG = "MediaMuxerWrapper"; |
|||
|
|||
private static final String DIR_NAME = "USBCameraTest"; |
|||
private static final SimpleDateFormat mDateTimeFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); |
|||
|
|||
private String mOutputPath; |
|||
private final MediaMuxer mMediaMuxer; // API >= 18
|
|||
private int mEncoderCount, mStatredCount; |
|||
private boolean mIsStarted; |
|||
private MediaEncoder mVideoEncoder, mAudioEncoder; |
|||
|
|||
public MediaMuxerWrapper(String path) throws IOException { |
|||
try { |
|||
// 保存到自定义路径还是手机默认Movies路径
|
|||
if (TextUtils.isEmpty(path)) |
|||
mOutputPath = getCaptureFile(Environment.DIRECTORY_MOVIES, ".mp4").toString(); |
|||
mOutputPath = path; |
|||
|
|||
} catch (final NullPointerException e) { |
|||
throw new RuntimeException("This app has no permission of writing external storage"); |
|||
} |
|||
mMediaMuxer = new MediaMuxer(mOutputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); |
|||
mEncoderCount = mStatredCount = 0; |
|||
mIsStarted = false; |
|||
} |
|||
|
|||
public String getOutputPath() { |
|||
return mOutputPath; |
|||
} |
|||
|
|||
public void prepare() throws IOException { |
|||
if (mVideoEncoder != null) |
|||
mVideoEncoder.prepare(); |
|||
if (mAudioEncoder != null) |
|||
mAudioEncoder.prepare(); |
|||
} |
|||
|
|||
public void startRecording() { |
|||
if (mVideoEncoder != null) |
|||
mVideoEncoder.startRecording(); |
|||
if (mAudioEncoder != null) |
|||
mAudioEncoder.startRecording(); |
|||
} |
|||
|
|||
public void stopRecording() { |
|||
if (mVideoEncoder != null) |
|||
mVideoEncoder.stopRecording(); |
|||
mVideoEncoder = null; |
|||
if (mAudioEncoder != null) |
|||
mAudioEncoder.stopRecording(); |
|||
mAudioEncoder = null; |
|||
} |
|||
|
|||
public synchronized boolean isStarted() { |
|||
return mIsStarted; |
|||
} |
|||
|
|||
//**********************************************************************
|
|||
//**********************************************************************
|
|||
/** |
|||
* assign encoder to this calss. this is called from encoder. |
|||
* @param encoder instance of MediaVideoEncoder or MediaAudioEncoder |
|||
*/ |
|||
/*package*/ void addEncoder(final MediaEncoder encoder) { |
|||
if (encoder instanceof MediaVideoEncoder) { |
|||
if (mVideoEncoder != null) |
|||
throw new IllegalArgumentException("Video encoder already added."); |
|||
mVideoEncoder = encoder; |
|||
} else if (encoder instanceof MediaSurfaceEncoder) { |
|||
if (mVideoEncoder != null) |
|||
throw new IllegalArgumentException("Video encoder already added."); |
|||
mVideoEncoder = encoder; |
|||
} else if (encoder instanceof MediaVideoBufferEncoder) { |
|||
if (mVideoEncoder != null) |
|||
throw new IllegalArgumentException("Video encoder already added."); |
|||
mVideoEncoder = encoder; |
|||
} else if (encoder instanceof MediaAudioEncoder) { |
|||
if (mAudioEncoder != null) |
|||
throw new IllegalArgumentException("Video encoder already added."); |
|||
mAudioEncoder = encoder; |
|||
} else |
|||
throw new IllegalArgumentException("unsupported encoder"); |
|||
mEncoderCount = (mVideoEncoder != null ? 1 : 0) + (mAudioEncoder != null ? 1 : 0); |
|||
} |
|||
|
|||
/** |
|||
* request start recording from encoder |
|||
* @return true when muxer is ready to write |
|||
*/ |
|||
/*package*/ synchronized boolean start() { |
|||
if (DEBUG) Log.v(TAG, "start:"); |
|||
mStatredCount++; |
|||
if ((mEncoderCount > 0) && (mStatredCount == mEncoderCount)) { |
|||
mMediaMuxer.start(); |
|||
mIsStarted = true; |
|||
notifyAll(); |
|||
if (DEBUG) Log.v(TAG, "MediaMuxer started:"); |
|||
} |
|||
return mIsStarted; |
|||
} |
|||
|
|||
/** |
|||
* request stop recording from encoder when encoder received EOS |
|||
*/ |
|||
/*package*/ synchronized void stop() { |
|||
if (DEBUG) Log.v(TAG, "stop:mStatredCount=" + mStatredCount); |
|||
mStatredCount--; |
|||
if ((mEncoderCount > 0) && (mStatredCount <= 0)) { |
|||
try { |
|||
mMediaMuxer.stop(); |
|||
} catch (final Exception e) { |
|||
Log.w(TAG, e); |
|||
} |
|||
mIsStarted = false; |
|||
if (DEBUG) Log.v(TAG, "MediaMuxer stopped:"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* assign encoder to muxer |
|||
* @param format |
|||
* @return minus value indicate error |
|||
*/ |
|||
/*package*/ synchronized int addTrack(final MediaFormat format) { |
|||
if (mIsStarted) |
|||
throw new IllegalStateException("muxer already started"); |
|||
final int trackIx = mMediaMuxer.addTrack(format); |
|||
if (DEBUG) Log.i(TAG, "addTrack:trackNum=" + mEncoderCount + ",trackIx=" + trackIx + ",format=" + format); |
|||
return trackIx; |
|||
} |
|||
|
|||
/** |
|||
* write encoded data to muxer |
|||
* @param trackIndex |
|||
* @param byteBuf |
|||
* @param bufferInfo |
|||
*/ |
|||
/*package*/ synchronized void writeSampleData(final int trackIndex, final ByteBuffer byteBuf, final MediaCodec.BufferInfo bufferInfo) { |
|||
if (mStatredCount > 0) |
|||
mMediaMuxer.writeSampleData(trackIndex, byteBuf, bufferInfo); |
|||
} |
|||
|
|||
//**********************************************************************
|
|||
//**********************************************************************
|
|||
/** |
|||
* generate output file |
|||
* @param type Environment.DIRECTORY_MOVIES / Environment.DIRECTORY_DCIM etc. |
|||
* @param ext .mp4(.m4a for audio) or .png |
|||
* @return return null when this app has no writing permission to external storage. |
|||
*/ |
|||
public static final File getCaptureFile(final String type, final String ext) { |
|||
final File dir = new File(Environment.getExternalStoragePublicDirectory(type), DIR_NAME); |
|||
Log.d(TAG, "path=" + dir.toString()); |
|||
dir.mkdirs(); |
|||
if (dir.canWrite()) { |
|||
return new File(dir, getDateTimeString() + ext); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* get current date and time as String |
|||
* @return |
|||
*/ |
|||
private static final String getDateTimeString() { |
|||
final GregorianCalendar now = new GregorianCalendar(); |
|||
return mDateTimeFormat.format(now.getTime()); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,196 @@ |
|||
/* |
|||
* UVCCamera |
|||
* library and sample to access to UVC web camera on non-rooted Android device |
|||
* |
|||
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
* |
|||
* All files in the folder are under this Apache License, Version 2.0. |
|||
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
|||
* may have a different license, see the respective files. |
|||
*/ |
|||
|
|||
package com.serenegiant.usb.encoder; |
|||
|
|||
import android.media.MediaCodec; |
|||
import android.media.MediaCodecInfo; |
|||
import android.media.MediaCodecList; |
|||
import android.media.MediaFormat; |
|||
import android.util.Log; |
|||
import android.view.Surface; |
|||
|
|||
import java.io.IOException; |
|||
|
|||
public class MediaSurfaceEncoder extends MediaEncoder implements IVideoEncoder { |
|||
private static final boolean DEBUG = true; // TODO set false on release
|
|||
private static final String TAG = "MediaSurfaceEncoder"; |
|||
|
|||
private static final String MIME_TYPE = "video/avc"; |
|||
// parameters for recording
|
|||
private final int mWidth, mHeight; |
|||
private static final int FRAME_RATE = 15; |
|||
private static final float BPP = 0.50f; |
|||
|
|||
private Surface mSurface; |
|||
|
|||
public MediaSurfaceEncoder(final MediaMuxerWrapper muxer, final int width, final int height, final MediaEncoderListener listener) { |
|||
super(muxer, listener); |
|||
if (DEBUG) Log.i(TAG, "MediaVideoEncoder: "); |
|||
mWidth = width; |
|||
mHeight = height; |
|||
} |
|||
|
|||
/** |
|||
* Returns the encoder's input surface. |
|||
*/ |
|||
public Surface getInputSurface() { |
|||
return mSurface; |
|||
} |
|||
|
|||
@Override |
|||
protected void prepare() throws IOException { |
|||
if (DEBUG) Log.i(TAG, "prepare: "); |
|||
mTrackIndex = -1; |
|||
mMuxerStarted = mIsEOS = false; |
|||
|
|||
final MediaCodecInfo videoCodecInfo = selectVideoCodec(MIME_TYPE); |
|||
if (videoCodecInfo == null) { |
|||
Log.e(TAG, "Unable to find an appropriate codec for " + MIME_TYPE); |
|||
return; |
|||
} |
|||
if (DEBUG) Log.i(TAG, "selected codec: " + videoCodecInfo.getName()); |
|||
|
|||
final MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight); |
|||
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // API >= 18
|
|||
format.setInteger(MediaFormat.KEY_BIT_RATE, calcBitRate()); |
|||
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); |
|||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10); |
|||
if (DEBUG) Log.i(TAG, "format: " + format); |
|||
|
|||
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); |
|||
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); |
|||
// get Surface for encoder input
|
|||
// this method only can call between #configure and #start
|
|||
mSurface = mMediaCodec.createInputSurface(); // API >= 18
|
|||
mMediaCodec.start(); |
|||
if (DEBUG) Log.i(TAG, "prepare finishing"); |
|||
if (mListener != null) { |
|||
try { |
|||
mListener.onPrepared(this); |
|||
} catch (final Exception e) { |
|||
Log.e(TAG, "prepare:", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected void release() { |
|||
if (DEBUG) Log.i(TAG, "release:"); |
|||
if (mSurface != null) { |
|||
mSurface.release(); |
|||
mSurface = null; |
|||
} |
|||
super.release(); |
|||
} |
|||
|
|||
private int calcBitRate() { |
|||
final int bitrate = (int)(BPP * FRAME_RATE * mWidth * mHeight); |
|||
Log.i(TAG, String.format("bitrate=%5.2f[Mbps]", bitrate / 1024f / 1024f)); |
|||
return bitrate; |
|||
} |
|||
|
|||
/** |
|||
* select the first codec that match a specific MIME type |
|||
* @param mimeType |
|||
* @return null if no codec matched |
|||
*/ |
|||
protected static final MediaCodecInfo selectVideoCodec(final String mimeType) { |
|||
if (DEBUG) Log.v(TAG, "selectVideoCodec:"); |
|||
|
|||
// get the list of available codecs
|
|||
final int numCodecs = MediaCodecList.getCodecCount(); |
|||
for (int i = 0; i < numCodecs; i++) { |
|||
final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); |
|||
|
|||
if (!codecInfo.isEncoder()) { // skipp decoder
|
|||
continue; |
|||
} |
|||
// select first codec that match a specific MIME type and color format
|
|||
final String[] types = codecInfo.getSupportedTypes(); |
|||
for (int j = 0; j < types.length; j++) { |
|||
if (types[j].equalsIgnoreCase(mimeType)) { |
|||
if (DEBUG) Log.i(TAG, "codec:" + codecInfo.getName() + ",MIME=" + types[j]); |
|||
final int format = selectColorFormat(codecInfo, mimeType); |
|||
if (format > 0) { |
|||
return codecInfo; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* select color format available on specific codec and we can use. |
|||
* @return 0 if no colorFormat is matched |
|||
*/ |
|||
protected static final int selectColorFormat(final MediaCodecInfo codecInfo, final String mimeType) { |
|||
if (DEBUG) Log.i(TAG, "selectColorFormat: "); |
|||
int result = 0; |
|||
final MediaCodecInfo.CodecCapabilities caps; |
|||
try { |
|||
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); |
|||
caps = codecInfo.getCapabilitiesForType(mimeType); |
|||
} finally { |
|||
Thread.currentThread().setPriority(Thread.NORM_PRIORITY); |
|||
} |
|||
int colorFormat; |
|||
for (int i = 0; i < caps.colorFormats.length; i++) { |
|||
colorFormat = caps.colorFormats[i]; |
|||
if (isRecognizedVideoFormat(colorFormat)) { |
|||
if (result == 0) |
|||
result = colorFormat; |
|||
break; |
|||
} |
|||
} |
|||
if (result == 0) |
|||
Log.e(TAG, "couldn't find a good color format for " + codecInfo.getName() + " / " + mimeType); |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* color formats that we can use in this class |
|||
*/ |
|||
protected static int[] recognizedFormats; |
|||
static { |
|||
recognizedFormats = new int[] { |
|||
// MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar,
|
|||
// MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
|
|||
// MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
|
|||
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface, |
|||
}; |
|||
} |
|||
|
|||
private static final boolean isRecognizedVideoFormat(final int colorFormat) { |
|||
if (DEBUG) Log.i(TAG, "isRecognizedVideoFormat:colorFormat=" + colorFormat); |
|||
final int n = recognizedFormats != null ? recognizedFormats.length : 0; |
|||
for (int i = 0; i < n; i++) { |
|||
if (recognizedFormats[i] == colorFormat) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,193 @@ |
|||
/* |
|||
* UVCCamera |
|||
* library and sample to access to UVC web camera on non-rooted Android device |
|||
* |
|||
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
* |
|||
* All files in the folder are under this Apache License, Version 2.0. |
|||
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
|||
* may have a different license, see the respective files. |
|||
*/ |
|||
|
|||
package com.serenegiant.usb.encoder; |
|||
|
|||
import android.media.MediaCodec; |
|||
import android.media.MediaCodecInfo; |
|||
import android.media.MediaCodecList; |
|||
import android.media.MediaFormat; |
|||
import android.util.Log; |
|||
|
|||
import java.io.IOException; |
|||
import java.nio.ByteBuffer; |
|||
|
|||
/** |
|||
* This class receives video images as ByteBuffer(strongly recommend direct ByteBuffer) as NV21(YUV420SP) |
|||
* and encode them to h.264. |
|||
* If you use this directly with IFrameCallback, you should know UVCCamera and it backend native libraries |
|||
* never execute color space conversion. This means that color tone of resulted movie will be different |
|||
* from that you expected/can see on screen. |
|||
*/ |
|||
public class MediaVideoBufferEncoder extends MediaEncoder implements IVideoEncoder { |
|||
private static final boolean DEBUG = true; // TODO set false on release
|
|||
private static final String TAG = "MediaVideoBufferEncoder"; |
|||
|
|||
private static final String MIME_TYPE = "video/avc"; |
|||
// parameters for recording
|
|||
private static final int FRAME_RATE = 15; |
|||
private static final float BPP = 0.50f; |
|||
|
|||
private final int mWidth, mHeight; |
|||
protected int mColorFormat; |
|||
|
|||
public MediaVideoBufferEncoder(final MediaMuxerWrapper muxer, final int width, final int height, final MediaEncoderListener listener) { |
|||
super(muxer, listener); |
|||
if (DEBUG) Log.i(TAG, "MediaVideoEncoder: "); |
|||
mWidth = width; |
|||
mHeight = height; |
|||
} |
|||
|
|||
public void encode(final ByteBuffer buffer) { |
|||
// if (DEBUG) Log.v(TAG, "encode:buffer=" + buffer);
|
|||
synchronized (mSync) { |
|||
if (!mIsCapturing || mRequestStop) return; |
|||
} |
|||
encode(buffer, buffer.capacity(), getPTSUs()); |
|||
} |
|||
|
|||
@Override |
|||
protected void prepare() throws IOException { |
|||
if (DEBUG) Log.i(TAG, "prepare: "); |
|||
mTrackIndex = -1; |
|||
mMuxerStarted = mIsEOS = false; |
|||
|
|||
final MediaCodecInfo videoCodecInfo = selectVideoCodec(MIME_TYPE); |
|||
if (videoCodecInfo == null) { |
|||
Log.e(TAG, "Unable to find an appropriate codec for " + MIME_TYPE); |
|||
return; |
|||
} |
|||
if (DEBUG) Log.i(TAG, "selected codec: " + videoCodecInfo.getName()); |
|||
|
|||
final MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight); |
|||
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, mColorFormat); |
|||
format.setInteger(MediaFormat.KEY_BIT_RATE, calcBitRate()); |
|||
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); |
|||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10); |
|||
if (DEBUG) Log.i(TAG, "format: " + format); |
|||
|
|||
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); |
|||
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); |
|||
mMediaCodec.start(); |
|||
if (DEBUG) Log.i(TAG, "prepare finishing"); |
|||
if (mListener != null) { |
|||
try { |
|||
mListener.onPrepared(this); |
|||
} catch (final Exception e) { |
|||
Log.e(TAG, "prepare:", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private int calcBitRate() { |
|||
final int bitrate = (int)(BPP * FRAME_RATE * mWidth * mHeight); |
|||
Log.i(TAG, String.format("bitrate=%5.2f[Mbps]", bitrate / 1024f / 1024f)); |
|||
return bitrate; |
|||
} |
|||
|
|||
/** |
|||
* select the first codec that match a specific MIME type |
|||
* @param mimeType |
|||
* @return null if no codec matched |
|||
*/ |
|||
@SuppressWarnings("deprecation") |
|||
protected final MediaCodecInfo selectVideoCodec(final String mimeType) { |
|||
if (DEBUG) Log.v(TAG, "selectVideoCodec:"); |
|||
|
|||
// get the list of available codecs
|
|||
final int numCodecs = MediaCodecList.getCodecCount(); |
|||
for (int i = 0; i < numCodecs; i++) { |
|||
final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); |
|||
|
|||
if (!codecInfo.isEncoder()) { // skipp decoder
|
|||
continue; |
|||
} |
|||
// select first codec that match a specific MIME type and color format
|
|||
final String[] types = codecInfo.getSupportedTypes(); |
|||
for (int j = 0; j < types.length; j++) { |
|||
if (types[j].equalsIgnoreCase(mimeType)) { |
|||
if (DEBUG) Log.i(TAG, "codec:" + codecInfo.getName() + ",MIME=" + types[j]); |
|||
final int format = selectColorFormat(codecInfo, mimeType); |
|||
if (format > 0) { |
|||
mColorFormat = format; |
|||
return codecInfo; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* select color format available on specific codec and we can use. |
|||
* @return 0 if no colorFormat is matched |
|||
*/ |
|||
protected static final int selectColorFormat(final MediaCodecInfo codecInfo, final String mimeType) { |
|||
if (DEBUG) Log.i(TAG, "selectColorFormat: "); |
|||
int result = 0; |
|||
final MediaCodecInfo.CodecCapabilities caps; |
|||
try { |
|||
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); |
|||
caps = codecInfo.getCapabilitiesForType(mimeType); |
|||
} finally { |
|||
Thread.currentThread().setPriority(Thread.NORM_PRIORITY); |
|||
} |
|||
int colorFormat; |
|||
for (int i = 0; i < caps.colorFormats.length; i++) { |
|||
colorFormat = caps.colorFormats[i]; |
|||
if (isRecognizedViewoFormat(colorFormat)) { |
|||
if (result == 0) |
|||
result = colorFormat; |
|||
break; |
|||
} |
|||
} |
|||
if (result == 0) |
|||
Log.e(TAG, "couldn't find a good color format for " + codecInfo.getName() + " / " + mimeType); |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* color formats that we can use in this class |
|||
*/ |
|||
protected static int[] recognizedFormats; |
|||
static { |
|||
recognizedFormats = new int[] { |
|||
// MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar,
|
|||
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar, |
|||
MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar, |
|||
// MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface,
|
|||
}; |
|||
} |
|||
|
|||
private static final boolean isRecognizedViewoFormat(final int colorFormat) { |
|||
if (DEBUG) Log.i(TAG, "isRecognizedViewoFormat:colorFormat=" + colorFormat); |
|||
final int n = recognizedFormats != null ? recognizedFormats.length : 0; |
|||
for (int i = 0; i < n; i++) { |
|||
if (recognizedFormats[i] == colorFormat) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,228 @@ |
|||
/* |
|||
* UVCCamera |
|||
* library and sample to access to UVC web camera on non-rooted Android device |
|||
* |
|||
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
* |
|||
* All files in the folder are under this Apache License, Version 2.0. |
|||
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
|||
* may have a different license, see the respective files. |
|||
*/ |
|||
|
|||
package com.serenegiant.usb.encoder; |
|||
|
|||
import android.media.MediaCodec; |
|||
import android.media.MediaCodecInfo; |
|||
import android.media.MediaCodecList; |
|||
import android.media.MediaFormat; |
|||
import android.util.Log; |
|||
import android.view.Surface; |
|||
|
|||
import com.serenegiant.glutils.EGLBase; |
|||
import com.serenegiant.glutils.RenderHandler; |
|||
|
|||
import java.io.IOException; |
|||
|
|||
/** |
|||
* Encode texture images as H.264 video |
|||
* using MediaCodec. |
|||
* This class render texture images into recording surface |
|||
* camera from MediaCodec encoder using Open GL|ES |
|||
*/ |
|||
public class MediaVideoEncoder extends MediaEncoder implements IVideoEncoder { |
|||
private static final boolean DEBUG = true; // TODO set false on release
|
|||
private static final String TAG = "MediaVideoEncoder"; |
|||
|
|||
private static final String MIME_TYPE = "video/avc"; |
|||
// parameters for recording
|
|||
private final int mWidth, mHeight; |
|||
private static final int FRAME_RATE = 15; |
|||
private static final float BPP = 0.50f; |
|||
|
|||
private RenderHandler mRenderHandler; |
|||
private Surface mSurface; |
|||
|
|||
public MediaVideoEncoder(final MediaMuxerWrapper muxer, final int width, final int height, final MediaEncoderListener listener) { |
|||
super(muxer, listener); |
|||
if (DEBUG) Log.i(TAG, "MediaVideoEncoder: "); |
|||
mRenderHandler = RenderHandler.createHandler(TAG); |
|||
mWidth = width; |
|||
mHeight = height; |
|||
} |
|||
|
|||
public boolean frameAvailableSoon(final float[] tex_matrix) { |
|||
boolean result; |
|||
if (result = super.frameAvailableSoon()) |
|||
mRenderHandler.draw(tex_matrix); |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* This method does not work correctly on this class, |
|||
* use #frameAvailableSoon(final float[]) instead |
|||
* @return |
|||
*/ |
|||
@Override |
|||
public boolean frameAvailableSoon() { |
|||
boolean result; |
|||
if (result = super.frameAvailableSoon()) |
|||
mRenderHandler.draw(null); |
|||
return result; |
|||
} |
|||
|
|||
@Override |
|||
protected void prepare() throws IOException { |
|||
if (DEBUG) Log.i(TAG, "prepare: "); |
|||
mTrackIndex = -1; |
|||
mMuxerStarted = mIsEOS = false; |
|||
|
|||
final MediaCodecInfo videoCodecInfo = selectVideoCodec(MIME_TYPE); |
|||
if (videoCodecInfo == null) { |
|||
Log.e(TAG, "Unable to find an appropriate codec for " + MIME_TYPE); |
|||
return; |
|||
} |
|||
if (DEBUG) Log.i(TAG, "selected codec: " + videoCodecInfo.getName()); |
|||
|
|||
final MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight); |
|||
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // API >= 18
|
|||
format.setInteger(MediaFormat.KEY_BIT_RATE, calcBitRate()); |
|||
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); |
|||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10); |
|||
if (DEBUG) Log.i(TAG, "format: " + format); |
|||
|
|||
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); |
|||
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); |
|||
// get Surface for encoder input
|
|||
// this method only can call between #configure and #start
|
|||
mSurface = mMediaCodec.createInputSurface(); // API >= 18
|
|||
mMediaCodec.start(); |
|||
if (DEBUG) Log.i(TAG, "prepare finishing"); |
|||
if (mListener != null) { |
|||
try { |
|||
mListener.onPrepared(this); |
|||
} catch (final Exception e) { |
|||
Log.e(TAG, "prepare:", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void setEglContext(final EGLBase.IContext sharedContext, final int tex_id) { |
|||
mRenderHandler.setEglContext(sharedContext, tex_id, mSurface, true); |
|||
} |
|||
|
|||
@Override |
|||
protected void release() { |
|||
if (DEBUG) Log.i(TAG, "release:"); |
|||
if (mSurface != null) { |
|||
mSurface.release(); |
|||
mSurface = null; |
|||
} |
|||
if (mRenderHandler != null) { |
|||
mRenderHandler.release(); |
|||
mRenderHandler = null; |
|||
} |
|||
super.release(); |
|||
} |
|||
|
|||
private int calcBitRate() { |
|||
final int bitrate = (int)(BPP * FRAME_RATE * mWidth * mHeight); |
|||
Log.i(TAG, String.format("bitrate=%5.2f[Mbps]", bitrate / 1024f / 1024f)); |
|||
return bitrate; |
|||
} |
|||
|
|||
/** |
|||
* select the first codec that match a specific MIME type |
|||
* @param mimeType |
|||
* @return null if no codec matched |
|||
*/ |
|||
protected static final MediaCodecInfo selectVideoCodec(final String mimeType) { |
|||
if (DEBUG) Log.v(TAG, "selectVideoCodec:"); |
|||
|
|||
// get the list of available codecs
|
|||
final int numCodecs = MediaCodecList.getCodecCount(); |
|||
for (int i = 0; i < numCodecs; i++) { |
|||
final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); |
|||
|
|||
if (!codecInfo.isEncoder()) { // skipp decoder
|
|||
continue; |
|||
} |
|||
// select first codec that match a specific MIME type and color format
|
|||
final String[] types = codecInfo.getSupportedTypes(); |
|||
for (int j = 0; j < types.length; j++) { |
|||
if (types[j].equalsIgnoreCase(mimeType)) { |
|||
if (DEBUG) Log.i(TAG, "codec:" + codecInfo.getName() + ",MIME=" + types[j]); |
|||
final int format = selectColorFormat(codecInfo, mimeType); |
|||
if (format > 0) { |
|||
return codecInfo; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* select color format available on specific codec and we can use. |
|||
* @return 0 if no colorFormat is matched |
|||
*/ |
|||
protected static final int selectColorFormat(final MediaCodecInfo codecInfo, final String mimeType) { |
|||
if (DEBUG) Log.i(TAG, "selectColorFormat: "); |
|||
int result = 0; |
|||
final MediaCodecInfo.CodecCapabilities caps; |
|||
try { |
|||
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); |
|||
caps = codecInfo.getCapabilitiesForType(mimeType); |
|||
} finally { |
|||
Thread.currentThread().setPriority(Thread.NORM_PRIORITY); |
|||
} |
|||
int colorFormat; |
|||
for (int i = 0; i < caps.colorFormats.length; i++) { |
|||
colorFormat = caps.colorFormats[i]; |
|||
if (isRecognizedVideoFormat(colorFormat)) { |
|||
if (result == 0) |
|||
result = colorFormat; |
|||
break; |
|||
} |
|||
} |
|||
if (result == 0) |
|||
Log.e(TAG, "couldn't find a good color format for " + codecInfo.getName() + " / " + mimeType); |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* color formats that we can use in this class |
|||
*/ |
|||
protected static int[] recognizedFormats; |
|||
static { |
|||
recognizedFormats = new int[] { |
|||
// MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar,
|
|||
// MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
|
|||
// MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
|
|||
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface, |
|||
}; |
|||
} |
|||
|
|||
private static final boolean isRecognizedVideoFormat(final int colorFormat) { |
|||
if (DEBUG) Log.i(TAG, "isRecognizedVideoFormat:colorFormat=" + colorFormat); |
|||
final int n = recognizedFormats != null ? recognizedFormats.length : 0; |
|||
for (int i = 0; i < n; i++) { |
|||
if (recognizedFormats[i] == colorFormat) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
} |
File diff suppressed because it is too large
Binary file not shown.
Loading…
Reference in new issue