h5 capture属性

accept 表示打开的系统文件目录,capture 表示的是系统所捕获的默认设备,camera:照相机;camcorder:摄像机;microphone :录音,其中还有一个属性multiple,支持多选,当支持多选时,multiple优先级高于capture,所以只用写成:

<input type="file" accept="image/*" capture="camera">
<input type="file" accept="video/*" capture="camcorder">
<input type="file" accept="audio/*" capture="microphone">

uni.chooseImage实现

!function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
        typeof define === 'function' && define.amd ? define(factory) :
            (global = global || self, global.ImgSelector = factory());
}(this, function () {

    let ImgSelector = {}

    function updateElementStyle(element, styles) {
        for (const attrName in styles) {
            element.style[attrName] = styles[attrName]
        }
    }

    const ALL = '*'

    function isWXEnv() {
        const ua = window.navigator.userAgent.toLowerCase()
        return ua.match(/MicroMessenger/i) && ua.match(/MicroMessenger/i)[0] === 'micromessenger';
    }

    /**
     *  分析智能手机浏览器 版本信息
     */

    const browser = {
        versions: function () {
            const u = navigator.userAgent,
                app = navigator.appVersion;
            return { //移动终端浏览器版本信息
                trident: u.indexOf('Trident') > -1, //IE内核
                presto: u.indexOf('Presto') > -1, //opera内核
                webKit: u.indexOf('AppleWebKit') > -1, //苹果、谷歌内核
                gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1, //火狐内核
                mobile: !!u.match(/AppleWebKit.*Mobile.*/) || !!u.match(/AppleWebKit/), //是否为移动终端
                ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), //ios终端
                android: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1, //android终端或者uc浏览器
                iPhone: u.indexOf('iPhone') > -1 || u.indexOf('Mac') > -1, //是否为iPhone或者QQHD浏览器
                iPad: u.indexOf('iPad') > -1, //是否iPad
                webApp: u.indexOf('Safari') == -1, //是否web应该程序,没有头部与底部
                isWeixin: u.toLowerCase().match(/MicroMessenger/i) == 'micromessenger',//  是否是微信打开的浏览器
                isMQQbrowser: u.toLowerCase().match(/MQQbrowser/i) == 'mqqbrowser' //  是否是微信打开的浏览器
            };
        }(),
        // language: (navigator.browserLanguage || navigator.language).toLowerCase()
    }

//  是否是 ios系统
    function isIos() {
        return browser.versions.ios || browser.versions.iPhone || browser.versions.iPad;
    }

// 是否是安卓系统
    function isAndroid() {
        return browser.versions.android
    }

// 是否是移动端
    function isMobile() {
        return browser.versions.mobile;
    }

    function isWeixin() {
        return browser.versions.isWeixin;
    }

    function isMQQbrowser() {
        return browser.versions.isMQQbrowser;
    }


    function createInput({count, sourceType, type, extension}) {
        const inputEl = document.createElement('input')
        inputEl.type = 'file'

        updateElementStyle(inputEl, {
            position: 'absolute',
            visibility: 'hidden',
            'z-index': -999,
            width: 0,
            height: 0,
            top: 0,
            left: 0
        })

        inputEl.accept = extension.map(item => {
            if (type !== ALL) {
                // 剔除.拼接在type后
                return `${type}/${item.replace('.', '')}`
            } else {
                // 在微信环境里,'.jpeg,.png' 会提示没有应用可执行此操作
                if (isWXEnv()) {
                    return '.'
                }
                // 在后缀前方加上.
                return item.indexOf('.') === 0 ? item : `.${item}`
            }
        }).join(',')

        if (count > 1) {
            inputEl.multiple = 'multiple'
        }
        // 经过测试,仅能限制只通过相机拍摄,不能限制只允许从相册选择。
        if (sourceType.length === 1 && sourceType[0] === 'camera') {
            inputEl.capture = 'camera'
        }
        if (isAndroid() && isWeixin() && !isMQQbrowser()) {
            console.log("强制添加摄像头调用功能");
            inputEl.capture = 'camera';
        }
        return inputEl
    }

    function hasOwn(obj, key) {
        const hasOwnProperty = Object.prototype.hasOwnProperty
        return hasOwnProperty.call(obj, key)
    }

    const files = {}

    /**
     * 从url读取File
     * @param {string} url
     */
    ImgSelector.urlToFile = function (url) {
        let file = files[url]
        if (file) {
            return Promise.resolve(file)
        }
        if (/^data:[a-z-]+\/[a-z-]+;base64,/.test(url)) {
            return Promise.resolve(ImgSelector.base64ToFile(url))
        }
        return new Promise((resolve, reject) => {
            let xhr = new XMLHttpRequest()
            xhr.open('GET', url, true)
            xhr.responseType = 'blob'
            xhr.onload = function () {
                resolve(this.response)
            }
            xhr.onerror = reject
            xhr.send()
        })
    }

    /**
     * base64转File
     * @param {string} base64
     * @return {File}
     */
    ImgSelector.base64ToFile = function (base64) {
        base64 = base64.split(',')
        var type = base64[0].match(/:(.*?);/)[1]
        var str = atob(base64[1])
        var n = str.length
        var array = new Uint8Array(n)
        while (n--) {
            array[n] = str.charCodeAt(n)
        }
        return ImgSelector.blobToFile(array, type)
    }

    /**
     * 简易获取扩展名
     * @param {string} type
     * @return {string}
     */
    function getExtname(type) {
        const extname = type.split('/')[1]
        return extname ? `.${extname}` : ''
    }

    /**
     * 简易获取文件名
     * @param {*} url
     */
    function getFileName(url) {
        url = url.split('#')[0].split('?')[0]
        const array = url.split('/')
        return array[array.length - 1]
    }

    /**
     * blob转File
     * @param {Blob} blob
     * @param {string} type
     * @return {File}
     */
    ImgSelector.blobToFile = function (blob, type) {
        if (!(blob instanceof File)) {
            type = type || blob.type || ''
            const filename = `${Date.now()}${getExtname(type)}`
            try {
                blob = new File([blob], filename, {type})
            } catch (error) {
                blob = blob instanceof Blob ? blob : new Blob([blob], {type})
                blob.name = blob.name || filename
            }
        }
        return blob
    }

    /**
     * 从本地file或者blob对象创建url
     * @param {Blob|File} file
     * @return {string}
     */
    ImgSelector.fileToUrl = function (file) {
        for (const key in files) {
            if (hasOwn(files, key)) {
                const oldFile = files[key]
                if (oldFile === file) {
                    return key
                }
            }
        }
        let url = (window.URL || window.webkitURL).createObjectURL(file)
        files[url] = file
        return url
    }

    function getSameOriginUrl(url) {
        const a = document.createElement('a')
        a.href = url
        if (a.origin === location.origin) {
            return Promise.resolve(url)
        }
        return ImgSelector.urlToFile(url).then(ImgSelector.fileToUrl)
    }

    function revokeObjectURL(url) {
        (window.URL || window.webkitURL).revokeObjectURL(url)
        delete files[url]
    }

    let imageInput = null

    ImgSelector.chooseImage = function ({
                                            count = 1,
                                            sizeType = ['original'],
                                            sourceType = ['camera', 'album'],
                                            // extension=['jpg','png','jpeg'],
                                            extension = ['*'],
                                            success
                                        }) {
        // TODO handle sizeType 尝试通过 canvas 压缩

        if (imageInput) {
            document.body.removeChild(imageInput)
            imageInput = null
        }

        imageInput = createInput({
            count,
            sourceType,
            extension,
            type: 'image'
        })
        document.body.appendChild(imageInput)

        imageInput.addEventListener('change', function (event) {
            const tempFiles = []
            const fileCount = event.target.files.length
            for (let i = 0; i < fileCount; i++) {
                const file = event.target.files[i]
                let filePath
                Object.defineProperty(file, 'path', {
                    get() {
                        filePath = filePath || ImgSelector.fileToUrl(file)
                        return filePath
                    }
                })
                if (i < count) tempFiles.push(file)
            }
            const res = {
                errMsg: 'chooseImage:ok',
                get tempFilePaths() {
                    return tempFiles.map(({path}) => path)
                },
                tempFiles: tempFiles
            }
            success(res)
            // TODO 用户取消选择时,触发 fail,目前尚未找到合适的方法。
        })

        imageInput.click()
    }
    ImgSelector.appendImgSelector = function (imgName, key, optional = false) {
        return `
                <div class="title-with-image">
                  <div class="choose-image" data-key="${key}">
                    <img src="" alt="" srcset=""  class="image-item">
                    <i class="image-item-delete" ></i>
                    <span class="optional-text">${optional ? '选填项' : '必填项'}</span>
                  </div>
                  <div class="img-item-title">${imgName}</div>
                </div>
        `
    }
    return ImgSelector
})

参考链接

浏览器 BOM api MediaDevices.getUserMedia()

H5部署后navigator获取不到mediaDevices

由于浏览器的安全策略导致的,有下面三种情况是可以调起设备的,也就是 navigator.mediaDevices 不为 undefined

  • 地址为 localhost:// 访问时
  • 地址为 https://
  • 为文件访问 file://

所以本地调试需要搭建 https 环境调试

vue 代码

<template>
  <div>
    <video id="videoCamera" :width="videoWidth" :height="videoHeight" autoplay></video>
    <canvas style="display:none;" id="canvasCamera" :width="videoWidth" :height="videoHeight"></canvas>

    <button plain @click="setImage()">手动拍照</button>
    <p class="fail_tips">拍照,请正脸面向摄像头</p>
    // 给外面盒子设置宽高,可以限制拍照图片的大小位置范围
    <div class="result_img">
      <img :src="imgSrc" alt class="tx_img" width="100%"/>
    </div>
    <p class="res_tips">效果展示</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 视频调用相关数据开始
      videoWidth: 245,
      videoHeight: 326,
      imgSrc: '',
      thisCancas: null,
      thisContext: null,
      thisVideo: null,
      openVideo: false,
      //视频调用相关数据结束
      postVideoImg: ''// 图片上传后获取的url链接
    }
  },
  mounted() {
    this.getCompetence() //调用摄像头
  },
  methods: {
    postImg() {
      let formData = new FormData()
      formData.append('file', this.base64ToFile(this.imgSrc, 'png'))
      formData.append('flag', 'videoImg')// 额外参数
      // 对应的后台上传图片接口 === app/StudentVideoController/uploadFile
      this.$axios.post('app/StudentVideoController/uploadFile', formData).then(res => {
        // console.log(res);
        if (res.data.code == '00') {
          // 图片文件传至后台 == 获取到该图片的url路径
          this.postVideoImg = res.data.FilePath
          //获得图片的url后,需要做什么
          //做的事情......
        }

      }).catch(error => {
        console.log(error)
      })

    },

    // 调用权限(打开摄像头功能)
    getCompetence() {
      let _this = this
      _this.thisCancas = document.getElementById('canvasCamera')
      _this.thisContext = this.thisCancas.getContext('2d')
      _this.thisVideo = document.getElementById('videoCamera')
      _this.thisVideo.style.display = 'block'
      // 获取媒体属性,旧版本浏览器可能不支持mediaDevices,我们首先设置一个空对象
      if (navigator.mediaDevices === undefined) {
        navigator.mediaDevices = {}
      }
      // 一些浏览器实现了部分mediaDevices,我们不能只分配一个对象
      // 使用getUserMedia,因为它会覆盖现有的属性。
      // 这里,如果缺少getUserMedia属性,就添加它。
      if (navigator.mediaDevices.getUserMedia === undefined) {
        navigator.mediaDevices.getUserMedia = function (constraints) {
          // 首先获取现存的getUserMedia(如果存在)
          let getUserMedia =
              navigator.webkitGetUserMedia ||
              navigator.mozGetUserMedia ||
              navigator.getUserMedia
          // 有些浏览器不支持,会返回错误信息  保持接口一致
          if (!getUserMedia) {//不存在则报错
            return Promise.reject(
                new Error('getUserMedia is not implemented in this browser')
            )
          }
          // 否则,使用Promise将调用包装到旧的navigator.getUserMedia
          return new Promise(function (resolve, reject) {
            getUserMedia.call(navigator, constraints, resolve, reject)
          })
        }
      }
      const constraints = {
        audio: false,
        video: {
          width: this.videoWidth,
          height: this.videoHeight,
          transform: 'scaleX(-1)',
          facingMode: 'environment'
        }
      }
      navigator.mediaDevices
          .getUserMedia(constraints)
          .then(function (stream) {
            // 旧的浏览器可能没有srcObject
            if ('srcObject' in _this.thisVideo) {
              _this.thisVideo.srcObject = stream
            } else {
              // 避免在新的浏览器中使用它,因为它正在被弃用。
              _this.thisVideo.src = window.URL.createObjectURL(stream)
            }
            _this.thisVideo.onloadedmetadata = function (e) {
              _this.thisVideo.play()
            }
          })
          .catch(err => {
            console.log(err)
          })
    },
    setImage() {
      let _this = this
      // canvas画图
      _this.thisContext.drawImage(_this.thisVideo, 0, 0)
      // 获取图片base64链接
      let image = this.thisCancas.toDataURL('image/png')
      _this.imgSrc = image//赋值并预览图片
      this.postImg() // 绘制完图片调用图片上传接口

    },
    // 关闭摄像头
    stopNavigator() {
      this.thisVideo.srcObject.getTracks()[0].stop()
    },

    // base64 转为 file
    base64ToFile(urlData, fileName) {
      let arr = urlData.split(',')
      let mime = arr[0].match(/:(.*?);/)[1]
      let bytes = atob(arr[1]) // 解码base64
      let n = bytes.length
      let ia = new Uint8Array(n)
      while (n--) {
        ia[n] = bytes.charCodeAt(n)
      }
      return new File([ia], fileName, {type: mime})
    }
  },
  destroyed: function () { // 离开当前页面
    this.stopNavigator() // 关闭摄像头
  }
}
</script>
<style>
.result_img {
  width: 146px;
  height: 195px;
  background: #D8D8D8;
}
</style>

参考链接

Last Updated:
Contributors: zonglinlee