对于iOS环境上,简单的两个配置就OK啦
即只需在配置里加上摄像头和麦克风的使用权限。具体做法是在App 的info.plist
中加入:
.NSMicrophoneUsageDescription
.NSCameraUsageDescription
就完事了!
对于Android环境,就会比较复杂一点点:
step1、我们需要实现一个自己的 WebChromeClient,其主要目的就是为了拦截FileChooser这个选择文件的动作:
这里,用户在h5上点击文件,我们以下环节实现的WebChromeClient中,基于不同Android的api版本中的回调函数会被触发:
代码语言:javascript复制public class EssWebChromeClient extends WebChromeClient {
private Activity mActivity;
public EssWebChromeClient(Activity activity) {
mActivity = activity;
}
///省略部分代码
// For Android >= 3.0
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
EssH5Sdk.getInstance().recordVideoForApiBelow21(uploadMsg, acceptType, mActivity);
}
// For Android >= 4.1
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
EssH5Sdk.getInstance().recordVideoForApiBelow21(uploadMsg, acceptType, mActivity);
}
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
if(EssH5Sdk.getInstance().recordVideoForApi21(webView, filePathCallback, mActivity,fileChooserParams)){
return true;
}else{
return super.onShowFileChooser(webView, filePathCallback, fileChooserParams);
}
}
}
这里我们注意以下,openFileChooser函数中会有一个acceptType的参数;
这个参数实际上是对应我们H5那个input框中的accept属性,需要我们关注:
accept
属性是一个字符串,它定义了文件 input 应该接受的文件类型。表示在 file
类型的 <input>
元素中用户可以选择的文件类型。每个唯一文件类型说明符可以采用下列形式之一:
- 一个以英文句号(".")开头的合法的不区分大小写的文件名扩展名。例如:
.jpg
,.pdf
或.doc
。 - 一个不带扩展名的 MIME 类型字符串。
- 字符串
audio/*
, 表示“任何音频文件”。 - 字符串
video/*
,表示 “任何视频文件”。 - 字符串
image/*
,表示 “任何图片文件”。
这里还有一个属性值得我们去关注:
capture
属性是一个字符串,如果accept
属性指出了 input 是图片或者视频类型,则它指定了使用哪个摄像头去这些数据。
值 :user
表示应该使用前置摄像头和/或麦克风。
值: environment
表示应该使用后置摄像头和/或麦克风。
step2、好了,当用户点击选择文件时,已经触发了我们的WebChromeClient中的选择文件的回调,接下来,我们实现原生拉起的想起拍照或者是:
代码语言:javascript复制 public void recordVideoForApiBelow21(ValueCallback<Uri> uploadCallback, String acceptType, Activity activity) {
if("image/*".equals(acceptType)){
setUploadMessage(uploadMsg);
startCamera(activity);
}else if ("video/*".equals(acceptType)) {
setUploadCallback(uploadCallback);
recordVideo(activity);
}
}
@TargetApi(21)
public boolean recordVideoForApi21(WebView webView, ValueCallback<Uri[]> filePathCallback, Activity activity, WebChromeClient.FileChooserParams fileChooserParams){
String acceptType = fileChooserParams.getAcceptTypes()[0];
if("image/*".equals(acceptType)){
setUploadCallbackV21(filePathCallback);
startCamera(activity);
return true;
}
if ("video/*".equals(acceptType) ){
setUploadCallbackV21(filePathCallback);
recordVideo(activity);
return true;
}
return false;
}
这里的我们注意到两个版本的api其实对于回调的形式是有区别的,21以上是接受一个Uri[]的callback,而低于21是接收一个Url的callback,这里注意一下就好,然后,我们看startCamera和recordVideo具体如何实现:
这里不妨先看一个简单的,如何录制视频:
代码语言:javascript复制private void recordVideo(Activity activity){
try {
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra("android.intent.extras.CAMERA_FACING", 1); // 调用前置摄像头
activity.startActivityForResult(intent, VIDEO_REQUEST);
} catch (Exception e) {
e.printStackTrace();
}
}
录制视频比较简单,当然我配置了默认拉起前置摄像头,基于具体业务场景,比如做人脸识别,有时候还是有一定的帮助的。
那么,录制玩视频,这个startActivityForResult
,就会有一个onActivityResult
的回调,我们去取他的Intent data,那么结果并调用相应的callback,应该还记得上面设置的按个callback吧:
if (requestCode == VIDEO_REQUEST) { //根据请求码判断返回的是否是h5刷脸结果
Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
Uri[] uris = result == null ? null : new Uri[]{result};
if (mUploadCallbackAboveL != null) {
mUploadCallbackV21.onReceiveValue(uris);
setUploadCallbackAboveL(null);
} else {
mUploadMessage.onReceiveValue(result);
setUploadMessage(null);
}
}
所以,我们看到了,无非就是基于不同的api来掉用起回调函数。
所以,同样的到来,拍照也是这样一个套路:
代码语言:javascript复制private void takeCamera(Activity activity) {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
takePictureIntent.putExtra("android.intent.extras.CAMERA_FACING", 0); // 调用后置摄像头
//https://ptyagicodecamp.github.io/accessing-pictures-using-fileprovider.html
if (takePictureIntent.resolveActivity(activity.getPackageManager()) != null) {
File photoFile = null;
try {
photoFile = createImageFile(activity);
takePictureIntent.putExtra("PhotoPath", mCameraFilePath);
} catch (IOException ex) {
Log.e("TAG", "Unable to create Image File", ex);
}
//适配7.0
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
if (photoFile != null) {
Uri photoURI = FileProvider.getUriForFile(activity,
"com.tencent.xxx.fileprovider", photoFile);
takePictureIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
}
} else {
if (photoFile != null) {
mCameraFilePath = "file:" photoFile.getAbsolutePath();
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(photoFile));
} else {
takePictureIntent = null;
}
}
}
activity.startActivityForResult(takePictureIntent, TAKE_PHOTO_REQUEST);
}
private File createImageFile(Activity activity) throws IOException {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(new Date());
String imageFileName = "JPEG_" timeStamp "_";
File storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(
imageFileName, /* 前缀 */
".jpg", /* 后缀 */
storageDir /* 文件夹 */
);
mCameraFilePath = image.getAbsolutePath();
return image;
}
等等,这里需要注意的是,7.0之后,Android系统不允许已file:的方式暴露文件,需要使用FileProvider,所以,这里需要在AndroidManifest.xml
配置文件中去什么一个provider:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.tencent.xxx.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
android:authorities的取值需要注意一致,不然getUriForFile肯定就是crash了,而且是一个JNI的crash,莫名其妙,让你定位问题都及其蛋疼。
file_path.xml的内容如下:
代码语言:javascript复制<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="Android/data/com.tencent.xxx/files/Pictures" />
</paths>
因为我们拍照存储的临时文件,防止在相册中:Environment.DIRECTORY_PICTURES,所以这里的path就是这个,当然,这个path你断点调试一下,抓一下photoFile 这个变量的路径,自然就知道改填啥了。
ok,依然是到了我们的onActivityResult
环节:
代码语言:javascript复制 if (requestCode == TAKE_PHOTO_REQUEST){
if ( resultCode != RESULT_OK){//用户取消,传回一个空
if (mUploadCallbackAboveL != null) {
mUploadCallbackAboveL.onReceiveValue(null);
setUploadCallbackV21(null);
} else if (mUploadMessage != null) {
mUploadMessage.onReceiveValue(null);
setUploadMessage(null);
}
return;
}
Uri result = (data == null) ? null : data.getData();
if (result == null && hasFile(mCameraFilePath)) {
result = Uri.fromFile(new File(mCameraFilePath));
}
Uri[] uris = result == null ? null : new Uri[]{result};
if (mUploadCallbackAboveL != null) {
mUploadCallbackAboveL.onReceiveValue(uris);
setUploadCallbackV21(null);
} else if (mUploadMessage != null) {
mUploadMessage.onReceiveValue(result);
setUploadMessage(null);
}
}
这里需要注意一下,无论用户取消还是最终选择了,这里的data始终是null,但是我们可以通过resultCode来区分是否用户取消,用户取消的话,回调函数传回一个null就OK啦。
以上,就是WebChromeClient的具体细节,实现好之后,我们需要和webview关联上:
代码语言:javascript复制mWebView.setWebChromeClient(new EssWebChromeClient(H5Activity.this));
至此,webview上实现h5拍照,和录像的功能就完成了。