Reference article:

  • Implement demo:www.freesion.com/article/676…
  • GetUserMedia:developer.mozilla.org/zh-CN/docs/…
  • Some questions collection: www.jb51.net/html5/72239…
  • Can’t use getUserMedia: stackoverflow.com/questions/5…

Results show

HTML part

It is mainly divided into four parts

  • Start button
  • The original camera
  • Custom camera (emphasis)
  • Upload the prompt
<div class="uploadFacePic">
  <! -- header ellipsis -->
 
  <! -- Start camera button -->
  <main style="marginTop:20px">
    <div class="btn" @click="openCamera">Enable face collection</div>
  </main>

  <! -- 1 Rollback scheme when the original camera is incompatible -->
  <input id="file" type="file" accept="image/*" capture="camera" style="display:none">


  <! -- 2 Custom camera -->
  <div style="width: 100%; position: fixed; left: 0; bottom: 0; top: 0; right: 0;" v-if="cameraShow">
    <! -- Top style -->
    <div style="width: 100%; position: fixed; left: 0; bottom: 90vh; top: 0; right: 0; background:black">  
    </div>
    

    <! -- Middle part -->
    <video style="height: 65vh; width: 100vw; position: fixed; top: 10vh; left: 0;"></video>
    <div style="width: 100%; position: fixed; left: 0; bottom: 25vh; top: 10vh; right: 0;">
      <! -- A. Display the viewfinder when shooting. Just change the viewfinder picture -->
      <img src=".. /.. /assets/qujing.png" alt="" v-if="status==1" style="width: 100%; height: 100%; Opacity: 0.8">
      <! -- B. Show the snapshot after shooting -->
      <img :src="imageUrl" alt="" v-if="status==2" style="width:100%; height:100%">
    </div>
      
    <! -- Bottom control part -->
    <div class="control">
        
      <! -- Before taking the photo -->
      <div class="control_before" v-if="status==1">
        <div class="control_before_top">photo</div>
        <div class="control_before_bottom">
          <div class="smaller" @click="cameraShow=false">cancel</div>
          <i class="iconfont icon-xiangji bigger" @click="snapPhoto"></i>
          <i class="iconfont icon-zhongxin small" @click="front = ! front"></i>
        </div>
      </div>
        
      <! -- After photo taken -->
      <div class="control_after" v-if="status==2">
        <div class="smaller" @click="status=1">A remake</div>
        <div class="smaller" @click="submitPhoto('custom')">Use pictures</div>
      </div>
        
    </div>
      
    <! - snap - >
    <canvas id="mycanvas"></canvas>
  </div>
    

  <! -- Prompt section -->
  <div class="tipinfo" v-if="tipVisible">
    <div class="successContent" v-if="tipInfo.result=='ok'">
      <van-icon name="passed"/>
      <div class="title">Acquisition success</div>
      <div class="info">Congratulations, complete face photo collection</div>
      <div class="btn" @click="tipVisible=false">{{' return '+ btntext}}</div>
    </div>
    <div class="failContent" v-else>
      <van-icon name="warning-o" />
      <div class="title">Acquisition failure</div>
      <div class="info">{{tipinfo.msg +', please rebeat '}}</div>
      <div class="btn" @click="tipVisible=false">{{' return '+ btntext}}</div>
    </div>
  </div>

</div>
Copy the code

Js part

Variable part:

data() {
  return {
    type:' './ / upload type update | the upload
    cameraShow:false.// Start the custom camera
    status:0./ / custom camera - shooting schedule: 0 1 | | not open open but didn't shoot 2 | open and has been taken
    imageUrl:' '.// Custom camera - capture URL
    front:true.// Custom camera - Front and back conversion (not verified)
      
    // Prompt section
    tipVisible:false.tipInfo: {result:'fail'.msg:'Failed to collect face'
    },// Upload the result
    btntext:' '.// Countdown text
    time:null./ / timer

    imageFile:' '.// Picture object
    
  };
},
Copy the code

Start the camera

OpenCamera: Mainly does some compatibility and rollback.

openCamera() {
  // 1. Display first, because this is where to get the video tag
  this.cameraShow=true
    
  // Constraints: Specifies the media type of the request and corresponding parameters
  var constraints={
    audio: false.video: {
      facingMode: (this.front? "user" : "environment")}}// 3. Compatible parts:
  // Older browsers may not implement mediaDevices at all, so we can set an empty object first
  if (navigator.mediaDevices === undefined) {
    navigator.mediaDevices = {};
  }
  // Some browsers partially support mediaDevices. We cannot set getUserMedia directly to the object
  // This may overwrite existing attributes. Here we will only add the getUserMedia property if it is not there.
  if (navigator.mediaDevices.getUserMedia === undefined) {
    navigator.mediaDevices.getUserMedia = function(constraints) {
      // First, get getUserMedia, if there is one
      var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia || navigator.oGetUserMedia;
      // Some browsers don't implement it at all - so return an error to the Promise reject to keep a unified interface
      if(! getUserMedia) {return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
      }
      // Otherwise wrap a Promise for the old navigator.getUserMedia method
      return new Promise(function(resolve, reject) { getUserMedia.call(navigator, constraints, resolve, reject); }); }}// 4. Get the video stream
  let that=this
  navigator.mediaDevices.getUserMedia(constraints)
  .then(function(stream) {
    // It is compatible
    let video=document.querySelector('video');
    video.srcObject = stream;
    video.onloadedmetadata = function(e) {
      video.play();
    };
    // Enter the custom shooting mode
    that.status=1 
  })
  .catch(function(err) {
    // This is not compatible
    console.log('nonono',err)
    // Call the original camera
    that.originCamera()
  });
},
Copy the code

Plan 1: Compatibility

SnapPhoto: capture

snapPhoto(){
  var canvas = document.querySelector('#mycanvas');
  var video = document.querySelector('video');
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;
  canvas.getContext('2d').drawImage(video,0.0);
    
  // Save as a file for subsequent uploads to the server (not yet practiced) -- > subsequent commits
  this.imageFile=this.canvasToFile(canvas)
    
  // blob to URL: for display
  let p=new Promise((resolve,reject) = >{
      canvas.toBlob(blob= >{
        let url=URL.createObjectURL(blob)
        resolve(url)
    });
  })
  let that=this
  p.then(value= >{
    that.imageUrl=value
    that.status=2// Indicates that the shooting is complete})},Copy the code

CanvasToFile: Canvas is converted to a file format for uploading to the server

canvasToFile(canvas){
  var dataurl = canvas.toDataURL("image/png");
  var arr = dataurl.split(', '),
      mime = arr[0].match(/ : (. *?) ; /) [1],
      bstr = atob(arr[1]),
      n = bstr.length,
      u8arr = new Uint8Array(n);
  while (n--) {
      u8arr[n] = bstr.charCodeAt(n);
  }
  var file = new File([u8arr], "phone.png", {type: mime});
  return file
},
Copy the code

Plan 2: Incompatible

OriginCamera: Calls the original camera

originCamera(){
  let that=this
  
  // Turn off the custom camera
  that.cameraShow=false
  let promise= new Promise(function (resolve, reject) {
      let file=document.getElementById('file')
      file.click()
      file.onchange = function (event) {
          if(! event) { reject('empty')}// Save the image file -- > submit it later
          let file=event.target.files[0]
          resolve(file)
      }
  })
  promise.then((value) = > {
      that.submitPhoto('origin',value)
    }
  )
},
Copy the code

Submit and upload tips

submitPhoto(type,file) {
  if(type=='origin') {this.imageFile=file
  }
  console.log("Submit".this.imageFile);
    
  // Upload here
  // let fd=new FormData()
  // fd.append("face_image",this.imageFile)
  / /...
    
  // Upload successfully:
  this.tipInfo.result='ok'
  // this.tipInfo.result='fail'
    
  this.cameraShow=false
  
  // Start prompt
  this.countdown()
},

// Countdown and hints
countdown(){
  clearInterval(this.time);
  this.tipVisible=true
  let coden = 3;
  this.btntext = '('+coden+'s)';
  this.time = setInterval(() = > {
    coden--
    this.btntext = '('+coden+'s)';
    if (coden == 0) {
      clearInterval(this.time);
      this.tipVisible = false;
      this.btntext = ""; }},1000);
},
Copy the code

The CSS part

.uploadFacePic{
  .img{
    width:100%;
    height:100%
  }
  
  .bigger{
    font-weight: 600;
    font-size: 3em;
  }
  .small{
    font-size: 2em;
  }
  .smaller{
    font-size: 1.2 em; } // console.control{
    width: 100%;
    position: fixed; 
    left: 0; 
    bottom: 0; 
    top: 75vh; 
    right: 0;
    background:black;
    .control_before{
      position:relative;
      width:100%;
      height:100%;
      display: flex;
      flex-direction: column;
      .control_before_top{
        z-index: 1;
        flex: 1;
        color: orange;
        display: flex;
        justify-content: center;
        align-items: center;
      }
      .control_before_bottom{
        flex: 2;
        display: flex;
        justify-content: space-around;
        color: white;
        align-items: center;
        margin-bottom: 1.5 em; }}.control_after{
      position:relative;
      width:100%;
      height:100%;
      display: flex;
      color: white;
      align-items: center;
      justify-content: space-around; }}main{
    text-align: center;
  }
  .tipinfo{
    z-index: 2;
    position: fixed;
    top:46px;
    left: 0;
    right: 0;
    bottom: 0;
    background: white;
    display: flex;
    justify-content: center;
    align-items: center;
    .successContent{
      .van-icon{
        color: #f68618;
        font-size: 5em;
      }
      .title{
        color: #f68618;
        font-size: 1.8 em; }}.failContent{
      .van-icon{
        color: red;
        font-size: 5em;
      }
      .title{
        color: red;
        font-size: 1.8 em; }}.info{
      margin: 1em 0 3em; }}.btn{
    height: 34px;
    width: 80vw;
    background: #f68618;
    color: white;
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius:.3em;
    margin: 0auto; }}Copy the code

Afterword.

  • Part of icon is imported from Ali, which is not written here.
  • Currently in WeChat developer tools to see the effect of custom camera, on a real machine (HTTP) also won’t start, to say the navigator. MediaDevices. GetUserMedia server need to use the HTTPS (not yet)
  • I don’t know how to deal with the mirror effect when I take a selfie with my custom camera.
  • It is estimated that a loading UI will be made during the intermediate upload

Optimization: Video adaptive sizing

In HTML, the size of the visible view displayed on the screen is defined from the beginning, but different devices have their size, so how can it be uniform and adaptive?

  • The video tag’s original object-fit attribute was contain. This caused a problem: there was no way to contain the visual view size we defined

    <video style="object-fit: cover"></video>
    Copy the code
  • But it’s not over yet. After understanding the principle of object-fit: Cover, canvas should also change accordingly. In the cover case, when the width and height are inconsistent, the overhang is clipped. In addition, the cover is adaptive, meaning that instead of clipping only from one side, the end water clipped equally from both sides.

    var canvas = document.querySelector('#mycanvas');
    var video = document.querySelector('video');
    
    // Based on object-fit: cover
    
    // Original width and height
    let width=video.videoWidth
    let height=video.videoHeight
    
    // Check whether the width and height are consistent
    constWIDTH_UNEQUAL_HEIGHT=width! =height// The size of the clipped side
    const SPAN=Math.abs(width-height)/2	
    
    // Define the clipping point x,y according to the difference.
    // Example: If the height is 10 higher than the width, then the top will be trimmed by 5, and the starting point y will be equal to 5, thus ensuring that the cropped image is visible
    letcut_x=WIDTH_UNEQUAL_HEIGHT? SPAN:0  // Trim the starting point x
    let cut_y=WIDTH_UNEQUAL_HEIGHT?0:SPAN  // Trim the starting point y
    
    // If it is too clipped, the width should be removed.
    letcut_after_width=cut_x! =0? width-2*SPAN:width 
    letcut_after_height=cut_y! =0? height-2*SPAN:height
    
    // Draw the canvas (clipped)
    canvas.width = cut_after_width;
    canvas.height = cut_after_height;
    canvas.getContext('2d').drawImage(video,cut_x,cut_y,cut_after_width,cut_after_height,0.0,cut_after_width,cut_after_height)
    Copy the code

    Ok to complete.

Optimization: Video Image flipping

Inspired by what happened earlier.

<video style="transform: rotateY(180deg);"></video>
Copy the code

Rewrite the canvas a little bit

let context=canvas.getContext('2d')
context.drawImage(video,cut_x,cut_y,cut_after_width,cut_after_height,0.0,cut_after_width,cut_after_height)

// Mirror flip is required for the front
if(this.front){
  context.scale(-1.1);
  context.drawImage(video,cut_x,cut_y, width*-1,height);
}
Copy the code

However, I found that the scale of ios cannot accept negative numbers.