Newer
Older
AegisforEcosystem / next / AR.js-3.4.0 / three.js / src / threex / arjs-source.js
@KAOKA Daisuke KAOKA Daisuke on 31 May 2022 16 KB into AR.js
const Source = function (parameters) {
  var _this = this;

  this.ready = false;
  this.domElement = null;

  // handle default parameters
  this.parameters = {
    // type of source - ['webcam', 'image', 'video']
    sourceType: "webcam",
    // url of the source - valid if sourceType = image|video
    sourceUrl: null,

    // Device id of the camera to use (optional)
    deviceId: null,

    // resolution of at which we initialize in the source image
    sourceWidth: 640,
    sourceHeight: 480,
    // resolution displayed for the source
    displayWidth: 640,
    displayHeight: 480,
  };
  //////////////////////////////////////////////////////////////////////////////
  //		setParameters
  //////////////////////////////////////////////////////////////////////////////
  setParameters(parameters);
  function setParameters(parameters) {
    if (parameters === undefined) return;
    for (var key in parameters) {
      var newValue = parameters[key];

      if (newValue === undefined) {
        console.warn("ArToolkitSource: '" + key + "' parameter is undefined.");
        continue;
      }

      var currentValue = _this.parameters[key];

      if (currentValue === undefined) {
        console.warn(
          "ArToolkitSource: '" + key + "' is not a property of this material."
        );
        continue;
      }

      _this.parameters[key] = newValue;
    }
  }

  this.onInitialClick = function () {
    if (this.domElement && this.domElement.play) {
      this.domElement.play().then(() => {});
    }
  };
};

//////////////////////////////////////////////////////////////////////////////
//		Code Separator
//////////////////////////////////////////////////////////////////////////////
Source.prototype.init = function (onReady, onError) {
  var _this = this;

  if (this.parameters.sourceType === "image") {
    var domElement = this._initSourceImage(onSourceReady, onError);
  } else if (this.parameters.sourceType === "video") {
    var domElement = this._initSourceVideo(onSourceReady, onError);
  } else if (this.parameters.sourceType === "webcam") {
    // var domElement = this._initSourceWebcamOld(onSourceReady)
    var domElement = this._initSourceWebcam(onSourceReady, onError);
  } else {
    console.assert(false);
  }

  // attach
  this.domElement = domElement;
  this.domElement.style.position = "absolute";
  this.domElement.style.top = "0px";
  this.domElement.style.left = "0px";
  this.domElement.style.zIndex = "-2";
  this.domElement.setAttribute("id", "arjs-video");

  return this;
  function onSourceReady() {
    if (!_this.domElement) {
      return;
    }

    document.body.appendChild(_this.domElement);
    window.dispatchEvent(
      new CustomEvent("arjs-video-loaded", {
        detail: {
          component: document.querySelector("#arjs-video"),
        },
      })
    );

    _this.ready = true;

    onReady && onReady();
  }
};

////////////////////////////////////////////////////////////////////////////////
//          init image source
////////////////////////////////////////////////////////////////////////////////

Source.prototype._initSourceImage = function (onReady) {
  // TODO make it static
  var domElement = document.createElement("img");
  domElement.src = this.parameters.sourceUrl;

  domElement.width = this.parameters.sourceWidth;
  domElement.height = this.parameters.sourceHeight;
  domElement.style.width = this.parameters.displayWidth + "px";
  domElement.style.height = this.parameters.displayHeight + "px";

  domElement.onload = onReady;
  return domElement;
};

////////////////////////////////////////////////////////////////////////////////
//          init video source
////////////////////////////////////////////////////////////////////////////////

Source.prototype._initSourceVideo = function (onReady) {
  // TODO make it static
  var domElement = document.createElement("video");
  domElement.src = this.parameters.sourceUrl;

  domElement.style.objectFit = "initial";

  domElement.autoplay = true;
  domElement.webkitPlaysinline = true;
  domElement.controls = false;
  domElement.loop = true;
  domElement.muted = true;

  // start the video on first click if not started automatically
  document.body.addEventListener("click", this.onInitialClick, { once: true });

  domElement.width = this.parameters.sourceWidth;
  domElement.height = this.parameters.sourceHeight;
  domElement.style.width = this.parameters.displayWidth + "px";
  domElement.style.height = this.parameters.displayHeight + "px";

  domElement.onloadeddata = onReady;
  return domElement;
};

////////////////////////////////////////////////////////////////////////////////
//          init webcam source
////////////////////////////////////////////////////////////////////////////////

Source.prototype._initSourceWebcam = function (onReady, onError) {
  var _this = this;

  // init default value
  onError =
    onError ||
    function (error) {
      var event = new CustomEvent("camera-error", { error: error });
      window.dispatchEvent(event);

      setTimeout(() => {
        if (!document.getElementById("error-popup")) {
          var errorPopup = document.createElement("div");
          errorPopup.innerHTML =
            "Webcam Error\nName: " + error.name + "\nMessage: " + error.message;
          errorPopup.setAttribute("id", "error-popup");
          document.body.appendChild(errorPopup);
        }
      }, 1000);
    };

  var domElement = document.createElement("video");
  domElement.setAttribute("autoplay", "");
  domElement.setAttribute("muted", "");
  domElement.setAttribute("playsinline", "");
  domElement.style.width = this.parameters.displayWidth + "px";
  domElement.style.height = this.parameters.displayHeight + "px";

  // check API is available
  if (
    navigator.mediaDevices === undefined ||
    navigator.mediaDevices.enumerateDevices === undefined ||
    navigator.mediaDevices.getUserMedia === undefined
  ) {
    if (navigator.mediaDevices === undefined)
      var fctName = "navigator.mediaDevices";
    else if (navigator.mediaDevices.enumerateDevices === undefined)
      var fctName = "navigator.mediaDevices.enumerateDevices";
    else if (navigator.mediaDevices.getUserMedia === undefined)
      var fctName = "navigator.mediaDevices.getUserMedia";
    else console.assert(false);
    onError({
      name: "",
      message: "WebRTC issue-! " + fctName + " not present in your browser",
    });
    return null;
  }

  // get available devices
  navigator.mediaDevices
    .enumerateDevices()
    .then(function (devices) {
      var userMediaConstraints = {
        audio: false,
        video: {
          facingMode: "environment",
          width: {
            ideal: _this.parameters.sourceWidth,
            // min: 1024,
            // max: 1920
          },
          height: {
            ideal: _this.parameters.sourceHeight,
            // min: 776,
            // max: 1080
          },
        },
      };

      if (null !== _this.parameters.deviceId) {
        userMediaConstraints.video.deviceId = {
          exact: _this.parameters.deviceId,
        };
      }

      // get a device which satisfy the constraints
      navigator.mediaDevices
        .getUserMedia(userMediaConstraints)
        .then(function success(stream) {
          // set the .src of the domElement
          domElement.srcObject = stream;

          var event = new CustomEvent("camera-init", { stream: stream });
          window.dispatchEvent(event);

          // start the video on first click if not started automatically
          document.body.addEventListener("click", _this.onInitialClick, {
            once: true,
          });

          onReady();
        })
        .catch(function (error) {
          onError({
            name: error.name,
            message: error.message,
          });
        });
    })
    .catch(function (error) {
      onError({
        message: error.message,
      });
    });

  return domElement;
};

////////////////////////////////////////////////////////////////////////////////
//          dispose source
////////////////////////////////////////////////////////////////////////////////

Source.prototype.dispose = function () {
  this.ready = false;

  switch (this.parameters.sourceType) {
    case "image":
      this._disposeSourceImage();
      break;

    case "video":
      this._disposeSourceVideo();
      break;

    case "webcam":
      this._disposeSourceWebcam();
      break;
  }

  this.domElement = null;

  document.body.removeEventListener("click", this.onInitialClick, {
    once: true,
  });
};

////////////////////////////////////////////////////////////////////////////////
//          dispose image source
////////////////////////////////////////////////////////////////////////////////

Source.prototype._disposeSourceImage = function () {
  var domElement = document.querySelector("#arjs-video");

  if (!domElement) {
    return;
  }

  domElement.remove();
};

////////////////////////////////////////////////////////////////////////////////
//          dispose video source
////////////////////////////////////////////////////////////////////////////////

Source.prototype._disposeSourceVideo = function () {
  var domElement = document.querySelector("#arjs-video");

  if (!domElement) {
    return;
  }

  // https://html.spec.whatwg.org/multipage/media.html#best-practices-for-authors-using-media-elements
  domElement.pause();
  domElement.removeAttribute("src");
  domElement.load();

  domElement.remove();
};

////////////////////////////////////////////////////////////////////////////////
//          dispose webcam source
////////////////////////////////////////////////////////////////////////////////

Source.prototype._disposeSourceWebcam = function () {
  var domElement = document.querySelector("#arjs-video");

  if (!domElement) {
    return;
  }

  // https://stackoverflow.com/a/12436772
  if (domElement.srcObject && domElement.srcObject.getTracks) {
    domElement.srcObject.getTracks().map((track) => track.stop());
  }

  domElement.remove();
};

//////////////////////////////////////////////////////////////////////////////
//		Handle Mobile Torch
//////////////////////////////////////////////////////////////////////////////
Source.prototype.hasMobileTorch = function () {
  var stream = arToolkitSource.domElement.srcObject;
  if (stream instanceof MediaStream === false) return false;

  if (this._currentTorchStatus === undefined) {
    this._currentTorchStatus = false;
  }

  var videoTrack = stream.getVideoTracks()[0];

  // if videoTrack.getCapabilities() doesnt exist, return false now
  if (videoTrack.getCapabilities === undefined) return false;

  var capabilities = videoTrack.getCapabilities();

  return capabilities.torch ? true : false;
};

/**
 * toggle the flash/torch of the mobile fun if applicable.
 * Great post about it https://www.oberhofer.co/mediastreamtrack-and-its-capabilities/
 */
Source.prototype.toggleMobileTorch = function () {
  // sanity check
  console.assert(this.hasMobileTorch() === true);

  var stream = arToolkitSource.domElement.srcObject;
  if (stream instanceof MediaStream === false) {
    if (!document.getElementById("error-popup")) {
      var errorPopup = document.createElement("div");
      errorPopup.innerHTML =
        "enabling mobile torch is available only on webcam";
      errorPopup.setAttribute("id", "error-popup");
      document.body.appendChild(errorPopup);
    }
    return;
  }

  if (this._currentTorchStatus === undefined) {
    this._currentTorchStatus = false;
  }

  var videoTrack = stream.getVideoTracks()[0];
  var capabilities = videoTrack.getCapabilities();

  if (!capabilities.torch) {
    if (!document.getElementById("error-popup")) {
      var errorPopup = document.createElement("div");
      errorPopup.innerHTML = "no mobile torch is available on your camera";
      errorPopup.setAttribute("id", "error-popup");
      document.body.appendChild(errorPopup);
    }
    return;
  }

  this._currentTorchStatus = this._currentTorchStatus === false ? true : false;
  videoTrack
    .applyConstraints({
      advanced: [
        {
          torch: this._currentTorchStatus,
        },
      ],
    })
    .catch(function (error) {
      console.log(error);
    });
};

Source.prototype.domElementWidth = function () {
  return parseInt(this.domElement.style.width);
};
Source.prototype.domElementHeight = function () {
  return parseInt(this.domElement.style.height);
};

////////////////////////////////////////////////////////////////////////////////
//          handle resize
////////////////////////////////////////////////////////////////////////////////

Source.prototype.onResizeElement = function () {
  var _this = this;
  var screenWidth = window.innerWidth;
  var screenHeight = window.innerHeight;

  // sanity check
  console.assert(arguments.length === 0);

  // compute sourceWidth, sourceHeight
  if (this.domElement.nodeName === "IMG") {
    var sourceWidth = this.domElement.naturalWidth;
    var sourceHeight = this.domElement.naturalHeight;
  } else if (this.domElement.nodeName === "VIDEO") {
    var sourceWidth = this.domElement.videoWidth;
    var sourceHeight = this.domElement.videoHeight;
  } else {
    console.assert(false);
  }

  // compute sourceAspect
  var sourceAspect = sourceWidth / sourceHeight;
  // compute screenAspect
  var screenAspect = screenWidth / screenHeight;

  // if screenAspect < sourceAspect, then change the width, else change the height
  if (screenAspect < sourceAspect) {
    // compute newWidth and set .width/.marginLeft
    var newWidth = sourceAspect * screenHeight;
    this.domElement.style.width = newWidth + "px";
    this.domElement.style.marginLeft = -(newWidth - screenWidth) / 2 + "px";

    // init style.height/.marginTop to normal value
    this.domElement.style.height = screenHeight + "px";
    this.domElement.style.marginTop = "0px";
  } else {
    // compute newHeight and set .height/.marginTop
    var newHeight = 1 / (sourceAspect / screenWidth);
    this.domElement.style.height = newHeight + "px";
    this.domElement.style.marginTop = -(newHeight - screenHeight) / 2 + "px";

    // init style.width/.marginLeft to normal value
    this.domElement.style.width = screenWidth + "px";
    this.domElement.style.marginLeft = "0px";
  }
};
/*
Source.prototype.copyElementSizeTo = function(otherElement){
	otherElement.style.width = this.domElement.style.width
	otherElement.style.height = this.domElement.style.height
	otherElement.style.marginLeft = this.domElement.style.marginLeft
	otherElement.style.marginTop = this.domElement.style.marginTop
}
*/

Source.prototype.copyElementSizeTo = function (otherElement) {
  if (window.innerWidth > window.innerHeight) {
    //landscape
    otherElement.style.width = this.domElement.style.width;
    otherElement.style.height = this.domElement.style.height;
    otherElement.style.marginLeft = this.domElement.style.marginLeft;
    otherElement.style.marginTop = this.domElement.style.marginTop;
  } else {
    //portrait
    otherElement.style.height = this.domElement.style.height;
    otherElement.style.width =
      (parseInt(otherElement.style.height) * 4) / 3 + "px";
    otherElement.style.marginLeft =
      (window.innerWidth - parseInt(otherElement.style.width)) / 2 + "px";
    otherElement.style.marginTop = 0;
  }
};

//////////////////////////////////////////////////////////////////////////////
//		Code Separator
//////////////////////////////////////////////////////////////////////////////

Source.prototype.copySizeTo = function () {
  console.warn(
    "obsolete function arToolkitSource.copySizeTo. Use arToolkitSource.copyElementSizeTo"
  );
  this.copyElementSizeTo.apply(this, arguments);
};

//////////////////////////////////////////////////////////////////////////////
//		Code Separator
//////////////////////////////////////////////////////////////////////////////

Source.prototype.onResize = function (arToolkitContext, renderer, camera) {
  if (arguments.length !== 3) {
    console.warn(
      "obsolete function arToolkitSource.onResize. Use arToolkitSource.onResizeElement"
    );
    return this.onResizeElement.apply(this, arguments);
  }

  var trackingBackend = arToolkitContext.parameters.trackingBackend;

  // RESIZE DOMELEMENT
  if (trackingBackend === "artoolkit") {
    this.onResizeElement();

    var isAframe = renderer.domElement.dataset.aframeCanvas ? true : false;
    if (isAframe === false) {
      this.copyElementSizeTo(renderer.domElement);
    } else {
    }

    if (arToolkitContext.arController !== null) {
      this.copyElementSizeTo(arToolkitContext.arController.canvas);
    }
  } else console.assert(false, "unhandled trackingBackend " + trackingBackend);

  // UPDATE CAMERA
  if (trackingBackend === "artoolkit") {
    if (arToolkitContext.arController !== null) {
      camera.projectionMatrix.copy(arToolkitContext.getProjectionMatrix());
    }
  } else console.assert(false, "unhandled trackingBackend " + trackingBackend);
};

export default Source;