show with app
library(shiny)
library(shinyvalidate)

ui <- fluidPage(
  div(id = "form",
    radioButtons("type", "Profile image type", choices = c(
      "Take selfie" = "selfie",
      "Upload image file" = "upload",
      "Gravatar" = "gravatar"
    )),
    conditionalPanel("input.type === 'selfie'",
      camera_input("selfie", "Take a selfie")
    ),
    conditionalPanel("input.type === 'upload'",
      fileInput("upload", NULL, accept = c("image/jpeg", "image/png")),
      uiOutput("upload_preview", container = tags$p)
    ),
    conditionalPanel("input.type === 'gravatar'",
      textInput("email", "Email address"),
      uiOutput("gravatar_preview", container = tags$p)
    ),
    actionButton("submit", "Submit", class = "btn-primary")
  )
)

server <- function(input, output, session) {
  iv <- InputValidator$new()
  
  selfie_iv <- InputValidator$new()
  selfie_iv$add_rule("selfie",
    sv_required("Click the 'Take photo' button before submitting"))
  selfie_iv$condition(~ input$type == "selfie")
  iv$add_validator(selfie_iv)
  
  upload_iv <- InputValidator$new()
  upload_iv$add_rule("upload", sv_required("Please choose a file"))
  upload_iv$add_rule("upload", function(upload) {
    if (!tools::file_ext(upload$name) %in% c("jpg", "jpeg", "png")) {
      "A JPEG or PNG file is required"
    }
  })
  upload_iv$condition(~ input$type == "upload")
  iv$add_validator(upload_iv)
  
  gravatar_iv <- InputValidator$new()
  gravatar_iv$add_rule("email", sv_required())
  gravatar_iv$add_rule("email", sv_email())
  gravatar_iv$condition(~ input$type == "gravatar")
  iv$add_validator(gravatar_iv)
  
  output$upload_preview <- renderUI({
    req(input$upload)
    req(nrow(input$upload) == 1)
    req(tools::file_ext(input$upload$name) %in% c("jpg", "jpeg", "png"))
    
    uri <- base64enc::dataURI(file = input$upload$datapath,
      mime = input$upload$type)
    tags$img(src = uri, style = htmltools::css(max_width = "300px", max_height = "300px"))
  })
  
  output$gravatar_preview <- renderUI({
    req(gravatar_iv$is_valid())
    
    email <- gsub("^\\s*(.*?)\\s*$", "\\1", input$email)
    email <- tolower(email)
    hash <- digest::digest(email, algo = "md5", serialize = FALSE)
    url <- paste0("https://www.gravatar.com/avatar/", hash)

    tags$img(src = url, alt = "Gravatar image")
  })
  
  observeEvent(input$submit, {
    if (iv$is_valid()) {
      removeUI("#form")
      insertUI(".container-fluid", "beforeEnd", "Thank you for your submission!")
      
      # input$selfie contains raw png data, suitable for png::readPNG()
    } else {
      iv$enable() # Start showing validation feedback
    }
  })
}

shinyApp(ui, server)
if (window.Shiny) {
  var cameraInputBinding = new Shiny.InputBinding();
  $.extend(cameraInputBinding, {
    find: function(scope) {
      return $(scope).find(".shiny-camera-input");
    },
    initialize: function(el) {
      const $el = $(el);
      new ShinyCameraInput(
        el,
        $el.children("video")[0],
        $el.children("canvas")[0],
        $el.find(">output>img")[0],
        $el.children("button.take")[0],
        $el.children("button.retake")[0]
      );
    },
    getType: function() {
      return "camera-datauri";
    },
    getValue: function(el) {
      if (el.classList.contains("shot")) {
        const img = el.querySelector("output img");
        if (img) {
          return img.src;
        }
      }
      return null;
    },
    setValue: function(el, value) {

    },
    subscribe: function(el, callback) {
      $(el).on("change.cameraInputBinding", function(e) {
        callback();
      });
    },
    unsubscribe: function(el) {
      $(el).off(".cameraInputBinding");
    },
    
    // The following two methods, setInvalid and clearInvalid, will be called
    // whenever this input fails or passes (respectively) validation.
    setInvalid: function(el, data) {
      el.classList.add("invalid");
      el.querySelector(".feedback-message").innerText = data.message;
    },
    clearInvalid: function(el) {
      el.classList.remove("invalid");
      el.querySelector(".feedback-message").innerText = "";
    }
  });
  Shiny.inputBindings.register(cameraInputBinding);
}

class ShinyCameraInput {

  constructor(container, video, canvas, photo, startbutton, restartbutton) {
    this.stream = null;
    this.streaming = false;

    this.container = container;
    this.video = video;
    this.canvas = canvas;
    this.photo = photo;
    this.startbutton = startbutton;
    this.restartbutton = restartbutton;
    this.width = this.container.getBoundingClientRect().width;
    this.height = 0; // This will be computed based on the input stream

    this.video.addEventListener('canplay', ev => {
      this.height = this.video.videoHeight / (this.video.videoWidth/this.width);
    
      this.container.style.width = this.width + "px";
      this.container.style.height = this.height + "px";
      this.container.classList.add("ready");

      this.video.setAttribute('width', this.width);
      this.video.setAttribute('height', this.height);
      this.canvas.setAttribute('width', this.width);
      this.canvas.setAttribute('height', this.height);
    }, false);
    this.startbutton.addEventListener('click', ev => {
      this.takepicture();
      ev.preventDefault();
    }, false);
    this.restartbutton.addEventListener('click', ev => {
      this.clearphoto();
      ev.preventDefault();
    }, false);
    this.clearphoto();
  }

  start() {
    navigator.mediaDevices.getUserMedia({ video: true, audio: false })
    .then(stream => {
      this.stream = stream;
      this.video.srcObject = stream;
      this.video.play();
    })
    .catch(function(err) {
      console.log("An error occurred: " + err);
    });
  }

  clearphoto() {
    var context = this.canvas.getContext('2d');
    context.fillStyle = "#AAA";
    context.fillRect(0, 0, this.canvas.width, this.canvas.height);

    var data = this.canvas.toDataURL('image/png');
    this.photo.setAttribute('src', data);
    this.container.classList.remove("shot");
    this.container.classList.remove("ready");
    $(this.container).trigger("change");
    this.start();
  }

  takepicture() {
    var context = this.canvas.getContext('2d');
    if (this.width && this.height) {
      this.canvas.width = this.width;
      this.canvas.height = this.height;
      context.drawImage(this.video, 0, 0, this.width, this.height);
    
      var data = this.canvas.toDataURL('image/png');
      this.photo.setAttribute('src', data);
      this.container.classList.add("shot");
      $(this.container).trigger("change");
      
      this.stream.getTracks().forEach(track => {
        if (track.readyState === "live") {
          track.stop();
        }
      });
    } else {
      this.clearphoto();
    }
  }
}
.shiny-camera-input {
  position: relative;
  margin-bottom: 1rem;
}

.shiny-camera-input .curtain {
  background-color: #CCC;
  height: 100%;
  min-height: 200px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: 6rem;
}
.shiny-camera-input.ready .curtain {
  display: none;
}

.shiny-camera-input button {
  position: absolute;
  right: 6px;
  bottom: 6px;
}

.shiny-camera-input canvas {
  display: none;
}

.shiny-camera-input video,
.shiny-camera-input output,
.shiny-camera-input button {
  display: none;
}

.shiny-camera-input.ready video {
  display: block;
}
.shiny-camera-input.ready output {
  display: none;
}
.shiny-camera-input.ready button.take {
  display: block;
}
.shiny-camera-input.ready button.retake {
  display: none;
}

.shiny-camera-input.shot video {
  display: none;
}
.shiny-camera-input.shot output {
  display: block;
}
.shiny-camera-input.shot button.take {
  display: none;
}
.shiny-camera-input.shot button.retake {
  display: block;
}

.shiny-camera-input output,
.shiny-camera-input img,
.shiny-camera-input video {
  padding: 0;
  margin: 0;
}

.shiny-camera-input .feedback-message {
  display: none;
  position: absolute;
  padding: 6px 8px;
  top: 0;
  left: 0;
  right: 0;
  height: auto;
  background-color: red;
  color: white;
}
.shiny-camera-input.ready .feedback-message {
  opacity: 0.7;
}
.shiny-camera-input.invalid .feedback-message {
  display: block;
}