因为遇到要求给手机调起的摄像头增加一个人像框,所以研究了一下。

这个项目是uniapp开发的手机端,uniapp中右camera组件可以直接实现调用手机的摄像头,但是它仅适用于小程序。

这个方法是参考了网上的案例,根据自己遇到的一些问题进行改进。

主要思路就是调用手机的摄像头数据,将他放到video标签里播放,通过渲染canvas来实现拍摄图片的功能。

话不多上,上代码

<template>
  <view class="" style="width: 100vw; height: 100vh; background: #1c1c1c">
    <video
      :class="
        facingMode == 'user'
          ? 'userVideo cameraViceo'
          : 'environemntVideo cameraViceo'
      "
      object-fit="fill"
    ></video>
    <view
      class=""
      style="
        width: 100vw;
        height: 100vh;
        background-color: transparent;
        opacity: 1;
        position: absolute;
        top: 0;
        left: 0;
        z-index: 1;
      "
    >
      <view
        class="flex justify-between"
        style="
          width: 100vw;
          height: 100rpx;
          padding: 0 20rpx;
          box-sizing: border-box;
          position: absolute;
          top: 0;
          left: 0;
          z-index: 6;
        "
      >
        <uni-icons
          v-if="!imageUrl"
          type="back"
          color="#fff"
          :size="32"
          @click="skipBack"
        >
        </uni-icons>
        <uni-icons
          v-if="!imageUrl"
          type="refreshempty"
          color="#fff"
          :size="32"
          @click="refreshCamera"
        >
        </uni-icons>
      </view>
      <view
        class="flex justify-center align-center"
        style="
          width: 100vw;
          height: calc(100vh - 240rpx)
          position: absolute;
          top: 100rpx;
          left: 0;
          z-index: 1;
        "
      >
        <image
          v-if="imageUrl"
          :src="imageUrl"
          mode="widthFix"
          style="width: 100vw"
        ></image>
      </view>
      <view
        class="flex justify-center align-center"
        style="
          width: 100vw;
          height: 100vh;
          position: absolute;
          top: 0;
          left: 0;
          z-index: 3;
        "
      >
        <image
          src="@/assets/wens/write/person.png"
          mode="widthFix"
          style="width: 100vw; margin-top: -200rpx"
        ></image>
      </view>

      <view
        class="flex justify-center"
        style="
          width: 100vw;
          height: 240rpx;
          position: absolute;
          bottom: 0;
          left: 0;
          z-index: 5;
        "
      >
        <view
          class=""
          style="
            position: absolute;
            bottom: 88rpx;
            left: 68rpx;
            color: #fff;
            border-radius: 16rpx;
            background: #3c3c3c;
            border-radius: 50%;
            line-height: 1;
            padding: 20rpx;
          "
        >
          <uni-icons
            v-if="imageUrl"
            type="closeempty"
            :size="24"
            color="#fff"
            @click="handleCancelPhoto"
          >
          </uni-icons>
          <uni-icons
            v-else
            type="loop"
            :size="24"
            color="#fff"
            @click="overturnCamera"
          >
          </uni-icons>
        </view>
        <view
          v-if="!imageUrl"
          class="outer-ring"
          style="position: absolute; bottom: 40rpx"
          @click="handlePhotographClick"
        >
          <view class="middle-ring">
            <view class="inner-ring"></view>
          </view>
        </view>
        <view
          class=""
          style="
            position: absolute;
            bottom: 88rpx;
            right: 68rpx;
            color: #fff;
            background: #3c3c3c;
            border-radius: 50%;
            line-height: 1;
            padding: 20rpx;
          "
        >
          <uni-icons
            v-if="imageUrl"
            color="#fff"
            type="checkmarkempty"
            :size="24"
            @click="skipBack"
          >
          </uni-icons>
          <uni-icons
            v-else
            type="images-filled"
            color="#fff"
            :size="24"
            @click="handleAddPhotographClick"
          >
          </uni-icons>
        </view>
      </view>
    </view>
  </view>
</template>
<script>
import uniIcons from "@/components/uni-ui/uni-icons/uni-icons.vue";
export default {
  components: {
    uniIcons,
  },
  data() {
    return {
      // 背景透明的取景框图片
      qjkImgSrc: "/static/images/qjk.png",
      // 背景透明的头像框图片
      qjtxkImgSrc: "/static/images/qjtxk.png",
      imageUrl: "",
      // 媒体流,用于关闭摄像头
      mediaStreamTrack: null,
      facingMode: "user",
      chooseImageFlag: false,
      chooseImageTimer: null,
    };
  },
  watch: {
    imageUrl: {
      handler(val) {
        if (val) {
          this.handlePhotographCloseClick();
        }
      },
    },
  },
  onLoad() {
    this.invokingCamera();
  },
  onShow() {
    setTimeout(() => {
      if (this.chooseImageFlag) {
        if (this.imageUrl) {
          this.handlePhotographCloseClick();
        } else {
          this.handlePhotographCloseClick();
          this.invokingCamera();
        }
      }
    }, 1000);
  },
  onUnload() {
    this.handlePhotographCloseClick();
  },
  methods: {
    invokingCamera() {
      const self = this;
      // 注意本例需要在HTTPS协议网站中运行,新版本Chrome中getUserMedia接口在http下不再支持。

      // 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象
      if (navigator.mediaDevices === undefined) {
        navigator.mediaDevices = {};
      }

      // 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
      // 因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。
      if (navigator.mediaDevices.getUserMedia === undefined) {
        navigator.mediaDevices.getUserMedia = function (constraints) {
          // 首先,如果有getUserMedia的话,就获得它
          const getUserMedia =
            navigator.getUserMedia ||
            navigator.webkitGetUserMedia ||
            navigator.mozGetUserMedia;

          // 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口
          if (!getUserMedia) {
            return Promise.reject(
              new Error("getUserMedia is not implemented in this browser")
            );
          }

          // 否则,为老的navigator.getUserMedia方法包裹一个Promise
          return new Promise(function (resolve, reject) {
            getUserMedia.call(navigator, constraints, resolve, reject);
          });
        };
      }

      uni.getSystemInfo({
        success: function (res) {
          const constraints = {
            audio: false,
            video: {
              // 前置摄像头 - user,后置摄像头 - environment
              facingMode: self.facingMode,
              // 手机端相当于高
              width: Math.max(res.windowWidth, res.windowHeight) - 120,
              // 手机端相当于宽
              height: Math.min(res.windowWidth, res.windowHeight),
            },
          };

          navigator.mediaDevices
            .getUserMedia(constraints)
            .then(function (stream) {
              self.mediaStreamTrack = stream;

              const video = document.querySelector("video");
              // 旧的浏览器可能没有srcObject
              if ("srcObject" in video) {
                video.srcObject = stream;
              } else {
                // 防止在新的浏览器里使用它,应为它已经不再支持了
                video.src = window.URL.createObjectURL(stream);
              }
              video.onloadedmetadata = function (e) {
                video.play();
                if (
                  video.videoWidth == 0 &&
                  video.videoHeight == 0 &&
                  video.paused
                ) {
                  video.load();
                }
              };
            })
            .catch(function (err) {
              console.log(err.name + ": " + err.message);
            });
        },
      });
    },
    refreshCamera() {
      if (!this.mediaStreamTrack) {
        this.invokingCamera();
        this.imageUrl = "";
      } else {
        this.handlePhotographCloseClick();
        this.invokingCamera();
        let video = document.querySelector("video");
        video.load();
      }
    },
    overturnCamera() {
      if (this.facingMode == "user") {
        this.facingMode = "environment";
      } else {
        this.facingMode = "user";
      }
      this.handlePhotographCloseClick();
      this.handleCancelPhoto();
    },
    handleCancelPhoto() {
      if (!this.mediaStreamTrack) {
        this.invokingCamera();
        this.imageUrl = "";
      }
    },
    handlePhotographCloseClick() {
      if (this.mediaStreamTrack) {
        // 关闭摄像头
        this.mediaStreamTrack.getTracks().forEach(function (track) {
          track.stop();
        });
        this.mediaStreamTrack = null;
      }
    },
    handlePhotographClick() {
      const self = this;
      const canvas = document.createElement("canvas");
      const ctx = canvas.getContext("2d");
      const video = document.querySelector("video");
      canvas.width = Math.min(video.videoWidth, video.videoHeight);
      canvas.height = Math.max(video.videoWidth, video.videoHeight);
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

      // ****** 镜像处理 ******
      function getPixel(imageData, row, column) {
        const uint8ClampedArray = imageData.data;
        const width = imageData.width;
        const height = imageData.height;
        const pixel = [];
        for (let i = 0; i < 4; i++) {
          pixel.push(uint8ClampedArray[row * width * 4 + column * 4 + i]);
        }
        return pixel;
      }

      function setPixel(imageData, row, column, pixel) {
        const uint8ClampedArray = imageData.data;
        const width = imageData.width;
        const height = imageData.height;
        for (let i = 0; i < 4; i++) {
          uint8ClampedArray[row * width * 4 + column * 4 + i] = pixel[i];
        }
      }

      const mirrorImageData = ctx.createImageData(canvas.width, canvas.height);
      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
      for (let h = 0; h < canvas.height; h++) {
        for (let w = 0; w < canvas.width; w++) {
          const pixel = getPixel(imageData, h, canvas.width - w - 1);
          setPixel(mirrorImageData, h, w, pixel);
        }
      }
      if (this.facingMode == "user") {
        ctx.putImageData(mirrorImageData, 0, 0);
      } else {
        ctx.putImageData(imageData, 0, 0);
      }
      // ****** 镜像处理 ******

      self.$nextTick(() => {
        const base64 = canvas.toDataURL("image/jpeg");
        self.imageUrl = base64;
        self.handlePhotographCloseClick();
      });
    },
    handleAddPhotographClick() {
      this.uploadImage();
    },
    uploadImage: function () {
      const self = this;
      this.chooseImageFlag = true;
      self.handlePhotographCloseClick();
      uni.chooseImage({
        count: 1, //默认9
        sizeType: ["original", "compressed"], //可以指定是原图还是压缩图,默认二者都有
        sourceType: ["album"], //从相册选择
        success: function (e) {
          if (e.tempFiles[0].size < 15 * 1024 * 1024) {
            self.imageUrl = e.tempFilePaths[0];
          } else {
            uni.showToast({
              title: "照片大小需小于15M",
              icon: "none",
            });
          }
        },
        fail: function (e) {
          // self.handlePhotographCloseClick();
        },
      });
    },
    skipBack() {
      if (this.imageUrl) {
        sessionStorage.setItem("cameraImg", this.imageUrl);
      }
      uni.navigateBack();
    },
  },
};
</script>
<style lang="scss" scoped>
.userVideo {
  transform: rotateY(180deg);
  -webkit-transform: rotateY(180deg);
  /* Safari 和 Chrome */
  -moz-transform: rotateY(180deg);
}
.cameraViceo {
  width: 100vw;
  height: calc(100vh - 240rpx);
}
/deep/ .uni-video-bar {
  display: none;
}

/deep/ .uni-video-cover {
  display: none;
}

.outer-ring {
  width: 160rpx;
  height: 160rpx;
  border-radius: 50%;
  background-color: #3c3c3c;
  display: flex;
  justify-content: center;
  align-items: center;
}

.middle-ring {
  width: 130rpx;
  height: 130rpx;
  border-radius: 50%;
  background-color: #000000;
  display: flex;
  justify-content: center;
  align-items: center;
}

.inner-ring {
  width: 130rpx;
  height: 130rpx;
  border-radius: 50%;
  background-color: #fff;
}
/deep/ .file-picker__box {
  width: 32px !important;
  height: 32px !important;
}
</style>

注意:存在三个问题(部分已解决)

1. 在浏览器上ios和Android调用都没问题,但是微信浏览器上ios系统中初次调用摄像头,请求摄像头权限后,会出现视频无显示,无法播放,就算一直重新调用invokingCamera()方法去重新渲染也无效。经研究是由于video宽高均为0,且为暂停状态导致的,不过退出页面,重新进入后又可以正常播放(我猜测是由于这次不需要重新请求摄像头权限)。所以解决方法为,在获取摄像头权限后,视频播放之后,让视频重新加载一次。

vue 调用IOS拍照 vue调用手机摄像头拍照_vue.js

这样就解决了ios系统在微信浏览器初次请求摄像头权限后无法显示画面的问题。

 2. 由于目前没有单独调取手机相册的api,所以都是两种都有(拍摄、相册),暂时没有方法避免。

3. 由于调用手机相册的功能只有三个监听事件(完成、成功、失败),监听不到取消操作,所以,目前解决方法是右上角增加了刷新图标,刷新一下,画面会重新出现。部分浏览器和机型支持取消操作后会重新调用app.vue里面的内容,所以在onshow生命周期里也写了延时器,会在取消操作1秒后重新加载摄像头(此事件只有部分机型和浏览器支持)。