ãã®è¨äºã§ã¯ãnavigator.mediaDevices.getUserMedia()
ã使ç¨ãã¦ã getUserMedia()
ã«å¯¾å¿ãã¦ããã³ã³ãã¥ã¼ã¿ã¼ãæºå¸¯é»è©±ã®ã«ã¡ã©ã«ã¢ã¯ã»ã¹ãã¦åçãæ®å½±ããæ¹æ³ã«ã¤ãã¦ç¤ºãã¦ãã¾ãã
ãæã¿ãªãã°ãã¢ã«ç´æ¥ã¸ã£ã³ããããã¨ãã§ãã¾ãã
HTML ã®ãã¼ã¯ã¢ããHTML ã®ã¤ã³ã¿ã¼ãã§ã¤ã¹ ã«ã¯ãã¹ããªã¼ã ã»ãã£ããã£ããã«ã¨ãã¬ã¼ã³ãã¼ã·ã§ã³ããã«ã®2ã¤ã®ä¸»è¦ãªæä½ã»ã¯ã·ã§ã³ãããã¾ãããããã¯ããããèªåèªèº«ã§ <div>
ã®ä¸ã«æ¨ªã«ä¸¦ãã§è¡¨ç¤ºãããã¹ã¿ã¤ã«è¨å®ãå¶å¾¡ã容æã«ã§ããããã«ãªã£ã¦ãã¾ãã
å·¦å´ã®æåã®ããã«ã«ã¯ 2 ã¤ã®æ§æè¦ç´ ãããã¾ãã <video>
è¦ç´ 㯠navigator.mediaDevices.getUserMedia()
ããã¹ããªã¼ã ãåãåãã <button>
ã¯ã¦ã¼ã¶ã¼ãã¯ãªãã¯ãããã¨ã§ãæ åã®ãã¬ã¼ã ããã£ããã£ãããã¨ãã§ãããã®ã§ãã
<div class="camera">
<video id="video">Video stream not available.</video>
<button id="startbutton">Take photo</button>
</div>
ããã¯ç´æçãªãã®ã§ã JavaScript ã®ã³ã¼ããåå¾ããã¨ãã«ãããããã©ã®ããã«çµã¿åãããã¦ãããã確èªã§ãã¾ãã
次ã«ã <canvas>
è¦ç´ ãããã¾ãããã®è¦ç´ ã«ãã£ããã£ãããã¬ã¼ã ãæ ¼ç´ããä½ããã®æ¹æ³ã§æä½ããå¾ãåºåç»åãã¡ã¤ã«ã«å¤æããå¯è½æ§ãããã¾ãããã®ãã£ã³ãã¹ã¯ã display
:none
ã§ã¹ã¿ã¤ã«è¨å®ãããã¨ã§é表示ã«ããç»é¢ãç
©éã«ãªããªãããã«ãã¦ãã¾ãï¼ã¦ã¼ã¶ã¼ã¯ãã®ä¸é段éãè¦ãå¿
è¦ããªãããï¼ã
ã¾ããç»åãæç»ããããã® <img>
è¦ç´ ãä¿æãã¦ããããããã¦ã¼ã¶ã¼ã«è¦ããæçµçãªè¡¨ç¤ºã¨ãªãã¾ãã
<canvas id="canvas"> </canvas>
<div class="output">
<img id="photo" alt="The screen capture will appear in this box." />
</div>
ããã ãã§ããæ®ãã¯ãã¼ã¸ã¬ã¤ã¢ã¦ãã¨ããã®ãã¼ã¸ã¸ã®ãªã³ã¯ãæä¾ããã¡ãã£ã¨ããããã¹ãã ãã§ãã
JavaScript ã®ã³ã¼ãããã§ã JavaScript ã®ã³ã¼ããè¦ã¦ããã¾ãããã説æããããããã«ãããã¤ãä¸å£ãµã¤ãºã«åå²ãã¦èª¬æãã¾ãã
åæåå§ããã«ã¯ãã°ãã¼ãã«å¤æ°ãé¿ããããã«ã¹ã¯ãªããå ¨ä½ãç¡å颿°ã§ã©ãããã使ç¨ããæ§ã ãªå¤æ°ãè¨å®ãã¾ãã
(() => {
const width = 320; // We will scale the photo width to this
const height = 0; // This will be computed based on the input stream
const streaming = false;
let video = null;
let canvas = null;
let photo = null;
let startbutton = null;
夿°ã¯æ¬¡ã®éãã§ãã
width
å ¥åãããæ åã®ãµã¤ãºãä½ã§ãããåºæ¥ä¸ãã£ãç»åãå¹ 320 ãã¯ã»ã«ã«ãªãããã«æ¡å¤§ç¸®å°ãã¾ãã
height
ç»åã®åºåé«ãã¯ï¼ã¹ããªã¼ã ã® width
ã¨ã¢ã¹ãã¯ãæ¯ãæå®ãããå ´åã«è¨ç®ããã¾ãï¼
streaming
ç¾å¨ãã¢ã¯ãã£ããªåç»ã¹ããªã¼ã ãå®è¡ããã¦ãããå¦ãã示ãã¾ãã
video
ããã¯ãã¼ã¸ã®èªã¿è¾¼ã¿ãçµãã£ãå¾ã§ã <video>
è¦ç´ ã¸ã®åç
§ã«ãªãã¾ãã
canvas
ããã¯ãã¼ã¸ã®èªã¿è¾¼ã¿ãçµãã£ãå¾ã§ã <canvas>
è¦ç´ ã¸ã®åç
§ã«ãªãã¾ãã
photo
ããã¯ãã¼ã¸ã®èªã¿è¾¼ã¿ãçµãã£ãå¾ã§ã <img>
è¦ç´ ã¸ã®åç
§ã«ãªãã¾ãã
startbutton
ããã¯ãã£ããã£ãèµ·åããããã«ä½¿ç¨ããã <button>
è¦ç´ ã¸ã®åç
§ã«ãªãã¾ãã ãã¼ã¸ã®èªã¿è¾¼ã¿ãçµãã£ã¦ããåå¾ãã¾ãã
startup()
颿°ã¯ EventTarget.addEventListener
ã®æç¤ºã«ããããã¼ã¸ã®èªã¿è¾¼ã¿ãå®äºããã¨ãã«å®è¡ããã¾ãããã®é¢æ°ã®ä»äºã¯ãã¦ã¼ã¶ã¼ã®ã¦ã§ãã«ã¡ã©ã¸ã®ã¢ã¯ã»ã¹ããªã¯ã¨ã¹ãããåºåå
ã® <img>
ãæ¢å®ã®ç¶æ
ã§åæåããã«ã¡ã©ããããããã®åç»ã®ãã¬ã¼ã ãåä¿¡ããããã«å¿
è¦ãªã¤ãã³ããªã¹ãã¼ã確ç«ãããã¿ã³ãã¯ãªãã¯ãããã¨ãã«åå¿ãã¦ç»åããã£ããã£ãããã¨ã§ãã
æåã«ãã¢ã¯ã»ã¹ã§ããããã«ããå¿ è¦ããã主è¦ãªè¦ç´ ã¸ã®åç §ãåå¾ãã¾ãã
function startup() {
video = document.getElementById('video');
canvas = document.getElementById('canvas');
photo = document.getElementById('photo');
startbutton = document.getElementById('startbutton');
ã¡ãã£ã¢ã¹ããªã¼ã ã®åå¾
次ã®ä»äºã¯ãã¡ãã£ã¢ã¹ããªã¼ã ãåå¾ãããã¨ã§ãã
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((stream) => {
video.srcObject = stream;
video.play();
})
.catch((err) => {
console.error(`An error occurred: ${err}`);
});
ããã§ã¯ MediaDevices.getUserMedia()
ãå¼ã³åºãã¦ãï¼é³å£°ãªãã®ï¼æ åã¹ããªã¼ã ããªã¯ã¨ã¹ããã¦ãã¾ããããã¯ãããã¹ãè¿ããããã«æåã¨å¤±æã®ã³ã¼ã«ããã¯ãæ¥ç¶ãã¦ãã¾ãã
æåã³ã¼ã«ããã¯ã¯ãå
¥åã¨ã㦠stream
ãªãã¸ã§ã¯ããåãåãã¾ãããã㯠<video>
è¦ç´ ã®ã½ã¼ã¹ã§ãããæ°ããã¹ããªã¼ã ã«ãªãã¾ãã
ã¹ããªã¼ã ã <video>
è¦ç´ ã«ãªã³ã¯ãããã¨ã HTMLMediaElement.play()
ãå¼ã³åºãã¦åçãéå§ãããã¨ãã§ãã¾ãã
ã¨ã©ã¼ã³ã¼ã«ããã¯ã¯ãã¹ããªã¼ã ãéããã¨ããã¾ããããªãå ´åã«å¼ã³åºããã¾ããããã¯ä¾ãã°ãäºææ§ã®ããã«ã¡ã©ãæ¥ç¶ããã¦ããªãå ´åããã¦ã¼ã¶ã¼ãã¢ã¯ã»ã¹ãæå¦ããå ´åãªã©ã«èµ·ããã¾ãã
æ åã®åçãå§ã¾ãã®ãå¾ ã¡åãããHTMLMediaElement.play()
ã <video>
ã«å¯¾ãã¦å¼ã³åºããå¾ãæ åã®ã¹ããªã¼ã ãæµãå§ããã¾ã§ã«ï¼æå¾
ããçãæéã§ããï¼çµéããæå»ãããã¾ãããããªãã¾ã§ãããã¯ããããã¨ãé¿ããããã«ã video
ã« canplay
ã¤ãã³ãç¨ã®ã¤ãã³ããªã¹ãã¼ã追å ããæ åã®åçãå®éã«å§ã¾ãã¨é
ä¿¡ãããããã«ãã¾ãããã®ç¹ã§ã video
ãªãã¸ã§ã¯ãã®ãã¹ã¦ã®ããããã£ã¯ãã¹ããªã¼ã ã®å½¢å¼ã«åºã¥ãã¦è¨å®ããã¦ãã¾ãã
video.addEventListener(
"canplay",
(ev) => {
if (!streaming) {
height = (video.videoHeight / video.videoWidth) * width;
video.setAttribute("width", width);
video.setAttribute("height", height);
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
streaming = true;
}
},
false,
);
ãã®ã³ã¼ã«ããã¯ã¯ãåãã¦å¼ã³åºãããã¨ã以å¤ã¯ä½ããã¾ããããã®ãã¹ãã§ã¯ã夿° streaming
ã®å¤ã確èªãããã®ã¡ã½ãããæåã«å®è¡ãããã¨ã㯠false
ã«ãªã£ã¦ãããã¨ã確èªãã¾ãã
ãã®ã¡ã½ãããæåã«å®è¡ãããå ´åã¯ãåç»ã®å®éã®ãµã¤ãºã§ãã video.videoWidth
ã¨ã¬ã³ããªã³ã°ããå¹
ã§ãã width
ã®ãµã¤ãºå·®ã«åºã¥ãã¦ãåç»ã®é«ããè¨å®ãã¾ãã
æå¾ã«ãåç»ã¨ãã£ã³ãã¹ã®ä¸¡æ¹ã® width
㨠height
ããããããã®è¦ç´ ã® 2 ã¤ã®ããããã£ã®ããããã«å¯¾ã㦠Element.setAttribute()
ãå¼ã³åºãã¦ãé©åãªå¹
ã¨é«ããè¨å®ãããã¨ã«ãã£ã¦ãäºãã«ä¸è´ããããã«è¨å®ãã¾ããæå¾ã«ã誤ã£ã¦ãã®è¨å®ã³ã¼ããå度å®è¡ããªãããã«ã夿° streaming
ã« true
ãè¨å®ãã¦ãã¾ãã
ã¦ã¼ã¶ã¼ã startbutton
ãã¯ãªãã¯ãããã³ã«éæ¢ç»ãæ®å½±ããã«ã¯ããã¿ã³ã«ã¤ãã³ããªã¹ãã¼ã追å ãã¦ã click
ã¤ãã³ããçºè¡ãããã¨ãã«å¼ã³åºãããããã«ããå¿
è¦ãããã¾ãã
startbutton.addEventListener(
"click",
(ev) => {
takepicture();
ev.preventDefault();
},
false,
);
ãã®ã¡ã½ããã¯åç´ã§ãä¸è¨ã®ã¹ããªã¼ã ããã®ãã¬ã¼ã ã®ãã£ãã㣠ã§å®ç¾©ããã¦ãã takepicture()
颿°ãå¼ã³åºããå¾ãåãåã£ãã¤ãã³ãã§ Event.preventDefault()
ãå¼ã³ãã¯ãªãã¯å¦çãè¤æ°åè¡ãããªãããã«ããã ãã§ãã
startup()` ã¡ã½ããã«ã¯ãã㨠2 è¡ã®ã³ã¼ããããã ãã§ãã
ããã§ãä¸è¨ã®åçããã¯ã¹ã®ã¯ãªã¢ ã®é
ã§è¨è¿°ãã clearphoto()
ã¡ã½ãããå¼ã³åºããã¨ã«ãªãã¾ãã
åçããã¯ã¹ãã¯ãªã¢ããã«ã¯ãç»åã使ããããã <img>
è¦ç´ ã§ä½¿ç¨å¯è½ãªå½¢å¼ã«å¤æãã¦ãæãæè¿æ®å½±ãããã¬ã¼ã ã表示ããå¿
è¦ãããã¾ãããã®ã³ã¼ãã¯æ¬¡ã®ããã«ãªãã¾ãã
function clearphoto() {
const context = canvas.getContext("2d");
context.fillStyle = "#AAA";
context.fillRect(0, 0, canvas.width, canvas.height);
const data = canvas.toDataURL("image/png");
photo.setAttribute("src", data);
}
ã¾ãããªãã¹ã¯ãªã¼ã³ã¬ã³ããªã³ã°ã«ä½¿ç¨ããé表示㮠<canvas>
è¦ç´ ã¸ã®åç
§ãåå¾ãããã¨ããå§ãã¾ããæ¬¡ã«ãfillStyle
ã #AAA
(ããªãæããç°è²) ã«è¨å®ããfillRect()
ãå¼ã³åºãã¦ãã£ã³ãã¹å
¨ä½ããã®è²ã§å¡ãã¤ã¶ãã¾ãã
æå¾ã«ããã£ã³ãã¹ã PNG ç»åã«å¤æã㦠photo.setAttribute()
ãå¼ã³åºãããã£ããã£ãã鿢ç»ã表示ããã¦ãã¾ãã
å®ç¾©ããæå¾ã®é¢æ°ãããããã®ç¹ããã®æ¼ç¿ã®ãã¤ã³ãã§ãã takepicture()
颿°ã¯ãç¾å¨è¡¨ç¤ºããã¦ããåç»ãã¬ã¼ã ããã£ããã£ãã PNG ãã¡ã¤ã«ã«å¤æãã¦ããã£ããã£ãããã¬ã¼ã æ ã«è¡¨ç¤ºããã®ããã®ä»äºã§ããã³ã¼ãã¯æ¬¡ã®ããã«ãªãã¾ãã
function takepicture() {
const context = canvas.getContext("2d");
if (width && height) {
canvas.width = width;
canvas.height = height;
context.drawImage(video, 0, 0, width, height);
const data = canvas.toDataURL("image/png");
photo.setAttribute("src", data);
} else {
clearphoto();
}
}
ãã£ã³ãã¹ã®ã³ã³ãã³ããæä½ããå¿ è¦ãããã¨ãã¯ãã¤ã§ãããã§ãããã¾ãé表示ã®ãã£ã³ãã¹ã® 2D æç»ã³ã³ããã¹ããåå¾ãããã¨ããå§ãã¾ãã
次ã«ãå¹
ã¨é«ããã©ã¡ãã 0 ã§ãªãå ´åï¼å°ãªãã¨ãæå¹ãªç»åãã¼ã¿ãããå¯è½æ§ãããã¨ãããã¨ï¼ããã£ã³ãã¹ã®å¹
ã¨é«ãããã£ããã£ãããã¬ã¼ã ã®å¹
ã¨é«ãã«ä¸è´ããããã«è¨å®ãã drawImage()
ãå¼ã³åºãã¦åç»ã®ç¾å¨ã®ãã¬ã¼ã ãã³ã³ããã¹ãã«æããå
¨ä½ã®ç»åããã£ã³ãã¹ã§å¡ãã¤ã¶ãããã«ãã¾ãã
ã¡ã¢: ãã®ã¤ã³ã¿ã¼ãã§ã¤ã¹ã¯ãHTMLImageElement
ã弿°ã¨ãã¦åãå
¥ããä»»æã® API ãã㯠HTMLVideoElement
ã HTMLImageElement
ã®ããã«è¦ãããã¨ãå©ç¨ãã¦ãããåç»ã®ç¾å¨ã®ãã¬ã¼ã ãç»åã®ã³ã³ãã³ãã¨ãã¦è¡¨ç¤ºãããããã«å·¥å¤«ããã¦ãã¾ãã
ãã£ã³ãã¹ã«ã¯ãã£ããã£ããç»åãæ ¼ç´ãããããHTMLCanvasElement.toDataURL()
ãå¼ã³åºã㦠PNG å½¢å¼ã«å¤æããæå¾ã« photo.setAttribute()
ãå¼ã³åºãã¦ãã£ããã£ãã鿢ç»ããã¯ã¹ã«ãã®ç»åã表示ããã¾ãã
å©ç¨ã§ããæå¹ãªç»åããªãå ´åï¼ã¤ã¾ããwidth
㨠height
ãã©ã¡ãã 0 ã®å ´åï¼ã¯ãclearphoto()
ãå¼ã³åºãã¦ããã£ããã£ãããã¬ã¼ã ããã¯ã¹ã®ã³ã³ãã³ããæ¶å»ãã¾ãã
<div class="contentarea">
<h1>MDN - navigator.mediaDevices.getUserMedia(): Still photo capture demo</h1>
<p>
This example demonstrates how to set up a media stream using your built-in
webcam, fetch an image from that stream, and create a PNG using that image.
</p>
<div class="camera">
<video id="video">Video stream not available.</video>
<button id="startbutton">Take photo</button>
</div>
<canvas id="canvas"> </canvas>
<div class="output">
<img id="photo" alt="The screen capture will appear in this box." />
</div>
<p>
Visit our article
<a
href="https://developer.mozilla.org/ja/docs/Web/API/WebRTC_API/Taking_still_photos">
Taking still photos with WebRTC</a
>
to learn more about the technologies used here.
</p>
</div>
CSS
#video {
border: 1px solid black;
box-shadow: 2px 2px 3px black;
width: 320px;
height: 240px;
}
#photo {
border: 1px solid black;
box-shadow: 2px 2px 3px black;
width: 320px;
height: 240px;
}
#canvas {
display: none;
}
.camera {
width: 340px;
display: inline-block;
}
.output {
width: 340px;
display: inline-block;
vertical-align: top;
}
#startbutton {
display: block;
position: relative;
margin-left: auto;
margin-right: auto;
bottom: 32px;
background-color: rgba(0, 150, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.7);
box-shadow: 0px 0px 1px 2px rgba(0, 0, 0, 0.2);
font-size: 14px;
font-family: "Lucida Grande", "Arial", sans-serif;
color: rgba(255, 255, 255, 1);
}
.contentarea {
font-size: 16px;
font-family: "Lucida Grande", "Arial", sans-serif;
width: 760px;
}
JavaScript
(() => {
// The width and height of the captured photo. We will set the
// width to the value defined here, but the height will be
// calculated based on the aspect ratio of the input stream.
const width = 320; // We will scale the photo width to this
let height = 0; // This will be computed based on the input stream
// |streaming| indicates whether or not we're currently streaming
// video from the camera. Obviously, we start at false.
let streaming = false;
// The various HTML elements we need to configure or control. These
// will be set by the startup() function.
let video = null;
let canvas = null;
let photo = null;
let startbutton = null;
function showViewLiveResultButton() {
if (window.self !== window.top) {
// Ensure that if our document is in a frame, we get the user
// to first open it in its own tab or window. Otherwise, it
// won't be able to request permission for camera access.
document.querySelector(".contentarea").remove();
const button = document.createElement("button");
button.textContent = "View live result of the example code above";
document.body.append(button);
button.addEventListener("click", () => window.open(location.href));
return true;
}
return false;
}
function startup() {
if (showViewLiveResultButton()) {
return;
}
video = document.getElementById("video");
canvas = document.getElementById("canvas");
photo = document.getElementById("photo");
startbutton = document.getElementById("startbutton");
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((stream) => {
video.srcObject = stream;
video.play();
})
.catch((err) => {
console.error(`An error occurred: ${err}`);
});
video.addEventListener(
"canplay",
(ev) => {
if (!streaming) {
height = video.videoHeight / (video.videoWidth / width);
// Firefox currently has a bug where the height can't be read from
// the video, so we will make assumptions if this happens.
if (isNaN(height)) {
height = width / (4 / 3);
}
video.setAttribute("width", width);
video.setAttribute("height", height);
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
streaming = true;
}
},
false,
);
startbutton.addEventListener(
"click",
(ev) => {
takepicture();
ev.preventDefault();
},
false,
);
clearphoto();
}
// Fill the photo with an indication that none has been
// captured.
function clearphoto() {
const context = canvas.getContext("2d");
context.fillStyle = "#AAA";
context.fillRect(0, 0, canvas.width, canvas.height);
const data = canvas.toDataURL("image/png");
photo.setAttribute("src", data);
}
// Capture a photo by fetching the current contents of the video
// and drawing it into a canvas, then converting that to a PNG
// format data URL. By drawing it on an offscreen canvas and then
// drawing that to the screen, we can change its size and/or apply
// other changes before drawing it.
function takepicture() {
const context = canvas.getContext("2d");
if (width && height) {
canvas.width = width;
canvas.height = height;
context.drawImage(video, 0, 0, width, height);
const data = canvas.toDataURL("image/png");
photo.setAttribute("src", data);
} else {
clearphoto();
}
}
// Set up our event listener to run the startup process
// once loading is complete.
window.addEventListener("load", startup, false);
})();
çµæ ãã£ã«ã¿ã¼ã§æ¥½ãã
<video>
è¦ç´ ãããã¬ã¼ã ãåå¾ãããã¨ã«ãã£ã¦ãã¦ã¼ã¶ã¼ã®ã¦ã§ãã«ã¡ã©ããç»åããã£ããã£ãã¦ããã®ã§ãæ åã«ãã£ã«ã¿ã¼ã楽ãã广ãã¨ã¦ãç°¡åã«é©ç¨ãããã¨ãã§ãã¾ããçµå±ã®ã¨ããã filter
ããããã£ã使ç¨ãã¦è¦ç´ ã«é©ç¨ãã CSS ãã£ã«ã¿ã¼ã¯ããã£ããã£ããåçã«å½±é¿ãä¸ãã¾ãããããã®ãã£ã«ã¿ã¼ã¯ãåç´ãªãã®ï¼ç»åãç½é»ã«ããï¼ããæ¥µç«¯ãªãã®ï¼ã¬ã¦ã¹ã¼ãããè²ç¸å転ï¼ã¾ã§ã®ç¯å²ãããã¾ãã
ãã®å¹æã¯ä¾ãã°ã Firefox ã®éçºè ãã¼ã«ã®ã¹ã¿ã¤ã«ã¨ãã£ã¿ã¼ã§åçãããã¨ãã§ãã¾ããããæ¹ã®è©³ç´°ã¯ CSS ãã£ã«ã¿ã¼ã®ç·¨éãåç §ãã¦ãã ããã
ç¹å®ã®æ©å¨ã®ä½¿ç¨å¿
è¦ã«å¿ãã¦ã許å¯ãããåç»ã½ã¼ã¹ã®ã»ãããç¹å®ã®æ©å¨ã¾ãã¯ä¸é£ã®æ©å¨ã«å¶éãããã¨ãã§ãã¾ãããããè¡ãã«ã¯ãMediaDevices.enumerateDevices
ãå¼ã³åºãã¾ããå©ç¨ã§ããæ©å¨ãè¨è¿°ãã MediaDeviceInfo
ãªãã¸ã§ã¯ãã®é
åã§ãããã¹ãå±¥è¡ããããã許å¯ããããã®ãæ¢ãã getUserMedia()
ã«æ¸¡ããã MediaTrackConstraints
ãªãã¸ã§ã¯ãã§å¯¾å¿ãã deviceId
ã¾ã㯠deviceId
ãæå®ãã¾ãã
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4