webview拉起拍照和录像的爬坑终结篇

2021-01-23 16:01:59 浏览数 (1)

对于iOS环境上,简单的两个配置就OK啦

即只需在配置里加上摄像头和麦克风的使用权限。具体做法是在App 的info.plist中加入:

代码语言:javascript复制
.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吧:

代码语言:javascript复制
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:

代码语言:javascript复制
        <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拍照,和录像的功能就完成了。

0 人点赞