1. Install (UV4L)

更新日: 2026.02.23

概要

UV4L(User space Video4Linux)は、Raspberry PiなどのLinuxシステムで リアルタイムでオーディオ・ビデオストリーミングを実現するアプリです。

環境

ルータの下に以下の部品を配置しています。

UV4L Development Environment

OSのインストール

先ずは、Raspberry PIにOSをインストールします。 Raspberry PI Imagerを参照し以下の条件でインストール用SDを製作後 OSのインストールを行って下さい。

  • OS: 32-bit Lite (Bookworm no Desktop)
  • コンピュータ名: raspberrypi
  • WiFiとSSHのオプションをオン。(インストール後、直ぐに使用出来る状態)

UV4Lのインストールと実行

* ソースのダウンロードとインストール

~$ curl https://www.linux-projects.org/listing/uv4l_repo/lpkey.asc | sudo gpg --dearmor -o /usr/share/keyrings/uv4l-archive-keyring.gpg ~$ echo "deb https://www.linux-projects.org/listing/uv4l_repo/raspbian/stretch stretch main" | sudo tee /etc/apt/sources.list.d/uv4l.list ~$ sudo apt update ~$ sudo apt install uv4l uv4l-webrtc uv4l-uvc -y ~$ sudo apt update ~$ sudo apt install uv4l uv4l-webrtc uv4l-uvc
  • 必要なファイルのダウンロード。
  • ファイル /etc/apt/sources.listに deb https://www.linux-projects.org/listing/uv4l_repo/raspbian/stretch stretch main を追加
  • キーはstretchで登録して下さい。古いですがこのキーのソースしか有りませんでした。
  • sudo apt updateでキーを反映。
  • uv4l、 uv4l-webrtc、 uv4l-uvc の3つをインストールします。

* lsusb でUSBカメラのIDを確認。

~$ lsusb Bus 001 Device 004: ID 046d:0825 Logitech, Inc. Webcam C270 Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. SMC9514 Hub Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

カメラIDをオプション--device-idで指定して
uv4l --driver uvc --device-id 046d:0825 --config-file=/etc/uv4l/uv4l-uvc.conf
を実行。これでUV4Lが立ち上がります。/p>

ブラウザから接続

PCのブラウザのURL欄に、”http://raspberrypi.local:8090”と入力(Raspberry PI専用カメラの場合は:8080)。以下の様なHPが表示されればOKです。

UV4l Main Control Panel

左上のWeb RTCアイコンをクリックすると以下の様になります。

UV4l Streaming Control Panel

左下の”Call!"ボタンでStreaming開始。”Hang up"ボタンで停止です。

Raspberry PIから音声が来ない

当初Raspberry PIから映像は送信されて来るのですが、音声が送信されませんでした。 Audio configuration に音声に関する記述が有り、それによると "arecord -l" でオーディオの状態を確認しそれに合わせてファイル”/etc/asound.conf”の修正が必要との事。 今回はマイクがカード1だったので capture.pcm "plug:dsnoop:1" と修正。これで音声が聞ける様になりました。

/etc/asound.conf text
pcm.!default { type asym playback.pcm "plug:hw:0" capture.pcm "plug:dsnoop:1" }

UV4Lの自動起動

UV4Lのコンフィグファイル、”/etc/uv4l/uv4l-uvc.conf” の54行目辺りにWebカメラのIDを指定する箇所が有ります。 ここにカメラのIDを入力して保存すれば、Raspberry PI起動と同時にUV4Lが起動します。

##################################
# UVC driver options
##################################
# device-path = <bus>:<address>
# path to the USB device to detect in the form bus:address (as hexadecimal numbers)
device-id = 046d:0825
# alternative to 'device-path', vendor and product identifiers of the USB device

次回は

下記はWeb RTC HPページのソースコードです。

web_rtc.html HTML
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>UV4L WebRTC</title> <!--script src="https://raw.githubusercontent.com/dorukeker/gyronorm.js/master/dist/gyronorm.complete.min.js" async></script--> <!--script src="https://rawgit.com/dorukeker/gyronorm.js/master/dist/gyronorm.complete.min.js" async></script--> <script type="text/javascript"> function httpGetAsync(theUrl, callback) { try { var xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function () { if (xmlHttp.readyState == 4 && xmlHttp.status == 200) { callback(xmlHttp.responseText); } }; xmlHttp.open("GET", theUrl, true); // true for asynchronous xmlHttp.send(null); } catch (e) { console.error(e); } } function addGyronormScript() { var srcUrl = "https://rawgit.com/dorukeker/gyronorm.js/master/dist/gyronorm.complete.min.js" httpGetAsync(srcUrl, function (text) { var script = document.createElement("script"); script.setAttribute("src", srcUrl); document.getElementsByTagName("head")[0].appendChild(script); }); } var signalling_server_hostname = location.hostname || "192.168.3.3"; var signalling_server_address = signalling_server_hostname + ':' + (location.port || (location.protocol === 'https:' ? 443 : 80)); var isFirefox = typeof InstallTrigger !== 'undefined';// Firefox 1.0+ addEventListener("DOMContentLoaded", function () { document.getElementById('signalling_server').value = signalling_server_address; var cast_not_allowed = !('MediaSource' in window) || location.protocol !== "https:"; if (cast_not_allowed || !isFirefox) { if (document.getElementById('cast_tab')) document.getElementById('cast_tab').disabled = true; if (cast_not_allowed) { // chrome supports if run with --enable-usermedia-screen-capturing document.getElementById('cast_screen').disabled = true; } document.getElementById('cast_window').disabled = true; document.getElementById('cast_application').disabled = true; document.getElementById('note2').style.display = "none"; document.getElementById('note4').style.display = "none"; } else { document.getElementById('note1').style.display = "none"; document.getElementById('note3').style.display = "none"; } addGyronormScript(); }); var ws = null; var pc; var gn; var datachannel, localdatachannel; var audio_video_stream; var recorder = null; var recordedBlobs; var pcConfig = {/*sdpSemantics : "plan-b"*,*/ "iceServers": [ {"urls": ["stun:stun.l.google.com:19302", "stun:" + signalling_server_hostname + ":3478"]} ]}; var pcOptions = { optional: [ // Deprecated: //{RtpDataChannels: false}, //{DtlsSrtpKeyAgreement: true} ] }; var mediaConstraints = { optional: [], mandatory: { OfferToReceiveAudio: true, OfferToReceiveVideo: true } }; var keys = []; var trickle_ice = true; var remoteDesc = false; var iceCandidates = []; RTCPeerConnection = window.RTCPeerConnection || /*window.mozRTCPeerConnection ||*/ window.webkitRTCPeerConnection; RTCSessionDescription = /*window.mozRTCSessionDescription ||*/ window.RTCSessionDescription; RTCIceCandidate = /*window.mozRTCIceCandidate ||*/ window.RTCIceCandidate; navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia || navigator.msGetUserMedia; var URL = window.URL || window.webkitURL; function createPeerConnection() { try { var pcConfig_ = pcConfig; try { ice_servers = document.getElementById('ice_servers').value; if (ice_servers) { pcConfig_.iceServers = JSON.parse(ice_servers); } } catch (e) { alert(e + "\nExample: " + '\n[ {"urls": "stun:stun1.example.net"}, {"urls": "turn:turn.example.org", "username": "user", "credential": "myPassword"} ]' + "\nContinuing with built-in RTCIceServer array"); } console.log(JSON.stringify(pcConfig_)); pc = new RTCPeerConnection(pcConfig_, pcOptions); pc.onicecandidate = onIceCandidate; if ('ontrack' in pc) { pc.ontrack = onTrack; } else { pc.onaddstream = onRemoteStreamAdded; // deprecated } pc.onremovestream = onRemoteStreamRemoved; pc.ondatachannel = onDataChannel; console.log("peer connection successfully created!"); } catch (e) { console.error("createPeerConnection() failed"); } } function onDataChannel(event) { console.log("onDataChannel()"); datachannel = event.channel; event.channel.onopen = function () { console.log("Data Channel is open!"); document.getElementById('datachannels').disabled = false; }; event.channel.onerror = function (error) { console.error("Data Channel Error:", error); }; event.channel.onmessage = function (event) { console.log("Got Data Channel Message:", event.data); document.getElementById('datareceived').value = event.data; }; event.channel.onclose = function () { datachannel = null; document.getElementById('datachannels').disabled = true; console.log("The Data Channel is Closed"); }; } function onIceCandidate(event) { if (event.candidate && event.candidate.candidate) { var candidate = { sdpMLineIndex: event.candidate.sdpMLineIndex, sdpMid: event.candidate.sdpMid, candidate: event.candidate.candidate }; var request = { what: "addIceCandidate", data: JSON.stringify(candidate) }; ws.send(JSON.stringify(request)); } else { console.log("End of candidates."); } } function addIceCandidates() { iceCandidates.forEach(function (candidate) { pc.addIceCandidate(candidate, function () { console.log("IceCandidate added: " + JSON.stringify(candidate)); }, function (error) { console.error("addIceCandidate error: " + error); } ); }); iceCandidates = []; } function onRemoteStreamAdded(event) { console.log("Remote stream added:", event.stream); var remoteVideoElement = document.getElementById('remote-video'); remoteVideoElement.srcObect = event.stream; //remoteVideoElement.play(); } function onTrack(event) { console.log("Remote track!"); var remoteVideoElement = document.getElementById('remote-video'); remoteVideoElement.srcObject = event.streams[0]; //remoteVideoElement.play(); } function onRemoteStreamRemoved(event) { var remoteVideoElement = document.getElementById('remote-video'); remoteVideoElement.srcObject = null; remoteVideoElement.src = ''; // TODO: remove } function start() { if ("WebSocket" in window) { document.getElementById("stop").disabled = false; document.getElementById("start").disabled = true; document.documentElement.style.cursor = 'wait'; var server = document.getElementById("signalling_server").value.toLowerCase(); var protocol = location.protocol === "https:" ? "wss:" : "ws:"; ws = new WebSocket(protocol + '//' + server + '/stream/webrtc'); function call(stream) { iceCandidates = []; remoteDesc = false; createPeerConnection(); if (stream) { pc.addStream(stream); } var request = { what: "call", options: { force_hw_vcodec: document.getElementById("remote_hw_vcodec").checked, vformat: document.getElementById("remote_vformat").value, trickle_ice: trickleice_selection() } }; ws.send(JSON.stringify(request)); console.log("call(), request=" + JSON.stringify(request)); } ws.onopen = function () { console.log("onopen()"); audio_video_stream = null; var cast_mic = document.getElementById("cast_mic").checked; var cast_tab = document.getElementById("cast_tab") ? document.getElementById("cast_tab").checked : false; var cast_camera = document.getElementById("cast_camera").checked; var cast_screen = document.getElementById("cast_screen").checked; var cast_window = document.getElementById("cast_window").checked; var cast_application = document.getElementById("cast_application").checked; var echo_cancellation = document.getElementById("echo_cancellation").checked; var localConstraints = {}; if (cast_mic) { if (echo_cancellation) localConstraints['audio'] = isFirefox ? {echoCancellation: true} : {optional: [{echoCancellation: true}]}; else localConstraints['audio'] = isFirefox ? {echoCancellation: false} : {optional: [{echoCancellation: false}]}; } else if (cast_tab) { localConstraints['audio'] = {mediaSource: "audioCapture"}; } else { localConstraints['audio'] = false; } if (cast_camera) { localConstraints['video'] = true; } else if (cast_screen) { if (isFirefox) { localConstraints['video'] = {frameRate: {ideal: 30, max: 30}, //width: {min: 640, max: 960}, //height: {min: 480, max: 720}, mozMediaSource: "screen", mediaSource: "screen"}; } else { // chrome://flags#enable-usermedia-screen-capturing document.getElementById("cast_mic").checked = false; localConstraints['audio'] = false; // mandatory for chrome localConstraints['video'] = {'mandatory': {'chromeMediaSource':'screen'}}; } } else if (cast_window) localConstraints['video'] = {frameRate: {ideal: 30, max: 30}, //width: {min: 640, max: 960}, //height: {min: 480, max: 720}, mozMediaSource: "window", mediaSource: "window"}; else if (cast_application) localConstraints['video'] = {frameRate: {ideal: 30, max: 30}, //width: {min: 640, max: 960}, //height: {min: 480, max: 720}, mozMediaSource: "application", mediaSource: "application"}; else localConstraints['video'] = false; var localVideoElement = document.getElementById('local-video'); if (localConstraints.audio || localConstraints.video) { if (navigator.getUserMedia) { navigator.getUserMedia(localConstraints, function (stream) { audio_video_stream = stream; call(stream); localVideoElement.muted = true; localVideoElement.srcObject = stream; localVideoElement.play(); }, function (error) { stop(); alert("An error has occurred. Check media device, permissions on media and origin."); console.error(error); }); } else { console.log("getUserMedia not supported"); } } else { call(); } }; ws.onmessage = function (evt) { var msg = JSON.parse(evt.data); if (msg.what !== 'undefined') { var what = msg.what; var data = msg.data; } //console.log("message=" + msg); console.log("message =" + what); switch (what) { case "offer": pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)), function onRemoteSdpSuccess() { remoteDesc = true; addIceCandidates(); console.log('onRemoteSdpSucces()'); pc.createAnswer(function (sessionDescription) { pc.setLocalDescription(sessionDescription); var request = { what: "answer", data: JSON.stringify(sessionDescription) }; ws.send(JSON.stringify(request)); console.log(request); }, function (error) { alert("Failed to createAnswer: " + error); }, mediaConstraints); }, function onRemoteSdpError(event) { alert('Failed to set remote description (unsupported codec on this browser?): ' + event); stop(); } ); /* * No longer needed, it's implicit in "call" var request = { what: "generateIceCandidates" }; console.log(request); ws.send(JSON.stringify(request)); */ break; case "answer": break; case "message": alert(msg.data); break; case "iceCandidate": // when trickle is enabled if (!msg.data) { console.log("Ice Gathering Complete"); break; } var elt = JSON.parse(msg.data); let candidate = new RTCIceCandidate({sdpMLineIndex: elt.sdpMLineIndex, candidate: elt.candidate}); iceCandidates.push(candidate); if (remoteDesc) addIceCandidates(); document.documentElement.style.cursor = 'default'; break; case "iceCandidates": // when trickle ice is not enabled var candidates = JSON.parse(msg.data); for (var i = 0; candidates && i < candidates.length; i++) { var elt = candidates[i]; let candidate = new RTCIceCandidate({sdpMLineIndex: elt.sdpMLineIndex, candidate: elt.candidate}); iceCandidates.push(candidate); } if (remoteDesc) addIceCandidates(); document.documentElement.style.cursor = 'default'; break; } }; ws.onclose = function (evt) { if (pc) { pc.close(); pc = null; } document.getElementById("stop").disabled = true; document.getElementById("start").disabled = false; document.documentElement.style.cursor = 'default'; }; ws.onerror = function (evt) { alert("An error has occurred!"); ws.close(); }; } else { alert("Sorry, this browser does not support WebSockets."); } } function stop() { if (datachannel) { console.log("closing data channels"); datachannel.close(); datachannel = null; document.getElementById('datachannels').disabled = true; } if (localdatachannel) { console.log("closing local data channels"); localdatachannel.close(); localdatachannel = null; } if (audio_video_stream) { try { if (audio_video_stream.getVideoTracks().length) audio_video_stream.getVideoTracks()[0].stop(); if (audio_video_stream.getAudioTracks().length) audio_video_stream.getAudioTracks()[0].stop(); audio_video_stream.stop(); // deprecated } catch (e) { for (var i = 0; i < audio_video_stream.getTracks().length; i++) audio_video_stream.getTracks()[i].stop(); } audio_video_stream = null; } stop_record(); document.getElementById('remote-video').srcObject = null; document.getElementById('local-video').srcObject = null; document.getElementById('remote-video').src = ''; // TODO; remove document.getElementById('local-video').src = ''; // TODO: remove if (pc) { pc.close(); pc = null; } if (ws) { ws.close(); ws = null; } document.getElementById("stop").disabled = true; document.getElementById("start").disabled = false; document.documentElement.style.cursor = 'default'; } function mute() { var remoteVideo = document.getElementById("remote-video"); remoteVideo.muted = !remoteVideo.muted; } function pause() { var remoteVideo = document.getElementById("remote-video"); if (remoteVideo.paused) remoteVideo.play(); else remoteVideo.pause(); } function fullscreen() { var remoteVideo = document.getElementById("remote-video"); if (remoteVideo.requestFullScreen) { remoteVideo.requestFullScreen(); } else if (remoteVideo.webkitRequestFullScreen) { remoteVideo.webkitRequestFullScreen(); } else if (remoteVideo.mozRequestFullScreen) { remoteVideo.mozRequestFullScreen(); } } function handleDataAvailable(event) { //console.log(event); if (event.data && event.data.size > 0) { recordedBlobs.push(event.data); } } function handleStop(event) { console.log('Recorder stopped: ', event); document.getElementById('record').innerHTML = 'Start Recording'; recorder = null; var superBuffer = new Blob(recordedBlobs, {type: 'video/webm'}); var recordedVideoElement = document.getElementById('recorded-video'); recordedVideoElement.src = URL.createObjectURL(superBuffer); } function discard_recording() { var recordedVideoElement = document.getElementById('recorded-video'); recordedVideoElement.srcObject = null; recordedVideoElement.src = ''; } function stop_record() { if (recorder) { recorder.stop(); console.log("recording stopped"); document.getElementById('record-detail').open = true; } } function startRecording(stream) { recordedBlobs = []; var options = {mimeType: 'video/webm;codecs=vp9'}; if (!MediaRecorder.isTypeSupported(options.mimeType)) { console.log(options.mimeType + ' is not Supported'); options = {mimeType: 'video/webm;codecs=vp8'}; if (!MediaRecorder.isTypeSupported(options.mimeType)) { console.log(options.mimeType + ' is not Supported'); options = {mimeType: 'video/webm;codecs=h264'}; if (!MediaRecorder.isTypeSupported(options.mimeType)) { console.log(options.mimeType + ' is not Supported'); options = {mimeType: 'video/webm'}; if (!MediaRecorder.isTypeSupported(options.mimeType)) { console.log(options.mimeType + ' is not Supported'); options = {mimeType: ''}; } } } } try { recorder = new MediaRecorder(stream, options); } catch (e) { console.error('Exception while creating MediaRecorder: ' + e); alert('Exception while creating MediaRecorder: ' + e + '. mimeType: ' + options.mimeType); return; } console.log('Created MediaRecorder', recorder, 'with options', options); //recorder.ignoreMutedMedia = true; recorder.onstop = handleStop; recorder.ondataavailable = handleDataAvailable; recorder.onwarning = function (e) { console.log('Warning: ' + e); }; recorder.start(); console.log('MediaRecorder started', recorder); } function start_stop_record() { if (pc && !recorder) { var streams = pc.getRemoteStreams(); if (streams.length) { console.log("starting recording"); startRecording(streams[0]); document.getElementById('record').innerHTML = 'Stop Recording'; } } else { stop_record(); } } function download() { if (recordedBlobs !== undefined) { var blob = new Blob(recordedBlobs, {type: 'video/webm'}); var url = window.URL.createObjectURL(blob); var a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = 'video.webm'; document.body.appendChild(a); a.click(); setTimeout(function () { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 100); } } function remote_hw_vcodec_selection() { if (!document.getElementById('remote_hw_vcodec').checked) unselect_remote_hw_vcodec(); else select_remote_hw_vcodec(); } function remote_hw_vcodec_format_selection() { if (document.getElementById('remote_hw_vcodec').checked) remote_hw_vcodec_selection(); } function select_remote_hw_vcodec() { document.getElementById('remote_hw_vcodec').checked = true; var vformat = document.getElementById('remote_vformat').value; switch (vformat) { case '5': document.getElementById('remote-video').style.width = "320px"; document.getElementById('remote-video').style.height = "240px"; break; case '10': document.getElementById('remote-video').style.width = "320px"; document.getElementById('remote-video').style.height = "240px"; break; case '20': document.getElementById('remote-video').style.width = "352px"; document.getElementById('remote-video').style.height = "288px"; break; case '25': document.getElementById('remote-video').style.width = "640px"; document.getElementById('remote-video').style.height = "480px"; break; case '30': document.getElementById('remote-video').style.width = "640px"; document.getElementById('remote-video').style.height = "480px"; break; case '35': document.getElementById('remote-video').style.width = "800px"; document.getElementById('remote-video').style.height = "480px"; break; case '40': document.getElementById('remote-video').style.width = "960px"; document.getElementById('remote-video').style.height = "720px"; break; case '50': document.getElementById('remote-video').style.width = "1024px"; document.getElementById('remote-video').style.height = "768px"; break; case '55': document.getElementById('remote-video').style.width = "1280px"; document.getElementById('remote-video').style.height = "720px"; break; case '60': document.getElementById('remote-video').style.width = "1280px"; document.getElementById('remote-video').style.height = "720px"; break; case '63': document.getElementById('remote-video').style.width = "1280px"; document.getElementById('remote-video').style.height = "720px"; break; case '65': document.getElementById('remote-video').style.width = "1280px"; document.getElementById('remote-video').style.height = "768px"; break; case '70': document.getElementById('remote-video').style.width = "1280px"; document.getElementById('remote-video').style.height = "768px"; break; case '75': document.getElementById('remote-video').style.width = "1536px"; document.getElementById('remote-video').style.height = "768px"; break; case '80': document.getElementById('remote-video').style.width = "1280px"; document.getElementById('remote-video').style.height = "960px"; break; case '90': document.getElementById('remote-video').style.width = "1600px"; document.getElementById('remote-video').style.height = "768px"; break; case '95': document.getElementById('remote-video').style.width = "1640px"; document.getElementById('remote-video').style.height = "1232px"; break; case '97': document.getElementById('remote-video').style.width = "1640px"; document.getElementById('remote-video').style.height = "1232px"; break; case '98': document.getElementById('remote-video').style.width = "1792px"; document.getElementById('remote-video').style.height = "896px"; break; case '99': document.getElementById('remote-video').style.width = "1792px"; document.getElementById('remote-video').style.height = "896px"; break; case '100': document.getElementById('remote-video').style.width = "1920px"; document.getElementById('remote-video').style.height = "1080px"; break; case '105': document.getElementById('remote-video').style.width = "1920px"; document.getElementById('remote-video').style.height = "1080px"; break; default: document.getElementById('remote-video').style.width = "1280px"; document.getElementById('remote-video').style.height = "720px"; } /* // Disable video casting. Not supported at the moment with hw codecs. var elements = document.getElementsByName('video_cast'); for(var i = 0; i < elements.length; i++) { elements[i].checked = false; } */ } function unselect_remote_hw_vcodec() { document.getElementById('remote_hw_vcodec').checked = false; document.getElementById('remote-video').style.width = "640px"; document.getElementById('remote-video').style.height = "480px"; } function singleselection(name, id) { var old = document.getElementById(id).checked; var elements = document.getElementsByName(name); for (var i = 0; i < elements.length; i++) { elements[i].checked = false; } document.getElementById(id).checked = old ? true : false; /* // Disable video hw codec. Not supported at the moment when casting. if (name === 'video_cast') { unselect_remote_hw_vcodec(); } */ } function send_message() { var msg = document.getElementById('datamessage').value; datachannel.send(msg); console.log("message sent: ", msg); } function create_localdatachannel() { if (pc && localdatachannel) return; localdatachannel = pc.createDataChannel('datachannel'); localdatachannel.onopen = function(event) { if (localdatachannel.readyState === "open") { localdatachannel.send("datachannel created!"); } }; console.log("data channel created"); } function close_localdatachannel() { if (localdatachannel) { localdatachannel.close(); localdatachannel = null; } console.log("local data channel closed"); } function handleOrientation(event) { var data = { "do": { "alpha": event.alpha.toFixed(1), // In degree in the range [0,360] "beta": event.beta.toFixed(1), // In degree in the range [-180,180] "gamma": event.gamma.toFixed(1), // In degree in the range [-90,90] "absolute": event.absolute } }; if (datachannel) datachannel.send(JSON.stringify(data)); } function isGyronormPresent() { var url = "gyronorm.complete.min.js"; var scripts = document.getElementsByTagName('script'); for (var i = scripts.length; i--; ) { if (scripts[i].src.indexOf(url) > -1) return true; } return false; } function handleGyronorm(data) { // Process: // data.do.alpha ( deviceorientation event alpha value ) // data.do.beta ( deviceorientation event beta value ) // data.do.gamma ( deviceorientation event gamma value ) // data.do.absolute ( deviceorientation event absolute value ) // data.dm.x ( devicemotion event acceleration x value ) // data.dm.y ( devicemotion event acceleration y value ) // data.dm.z ( devicemotion event acceleration z value ) // data.dm.gx ( devicemotion event accelerationIncludingGravity x value ) // data.dm.gy ( devicemotion event accelerationIncludingGravity y value ) // data.dm.gz ( devicemotion event accelerationIncludingGravity z value ) // data.dm.alpha ( devicemotion event rotationRate alpha value ) // data.dm.beta ( devicemotion event rotationRate beta value ) // data.dm.gamma ( devicemotion event rotationRate gamma value ) if (datachannel && document.getElementById('orientationsend').checked) datachannel.send(JSON.stringify(data)); } function orientationsend_selection() { if (document.getElementById('orientationsend').checked) { if (isGyronormPresent()) { console.log("gyronorm.js library found!"); if (gn) { gn.setHeadDirection(); return; } try { gn = new GyroNorm(); } catch (e) { console.log(e); document.getElementById('orientationsend').checked = false; return; } var args = { frequency: 60, // ( How often the object sends the values - milliseconds ) gravityNormalized: true, // ( If the gravity related values to be normalized ) orientationBase: GyroNorm.GAME, // ( Can be GyroNorm.GAME or GyroNorm.WORLD. gn.GAME returns orientation values with respect to the head direction of the device. gn.WORLD returns the orientation values with respect to the actual north direction of the world. ) decimalCount: 1, // ( How many digits after the decimal point will there be in the return values ) logger: null, // ( Function to be called to log messages from gyronorm.js ) screenAdjusted: false // ( If set to true it will return screen adjusted values. ) }; gn.init(args).then(function () { gn.start(handleGyronorm); gn.setHeadDirection(); // only with gn.GAME }).catch(function (e) { console.log("DeviceOrientation or DeviceMotion might not be supported by this browser or device"); }); } if (!gn) { window.addEventListener('deviceorientation', handleOrientation, true); console.log("gyronorm.js library not found, using defaults"); } } else { if (!gn) { window.removeEventListener('deviceorientation', handleOrientation, true); } } } function getKeycodesArray(arr) { var newArr = new Array(); for (var i = 0; i < arr.length; i++) { if (typeof arr[i] == "number") { newArr[newArr.length] = arr[i]; } } return newArr; } function convertKeycodes(arr) { var map = { /*Space*/ 32: 57, /*Enter*/13: 28, /*Tab*/ 9: 15, /*Esc*/27: 1, /*Backspace*/8: 14, /*Shift*/16: 42, /*Control*/ 17: 29, /*Alt Left*/ 18: 56, /*Alt Right*/ 225: 100, /*Caps Lock*/ 20: 58, /*Num Lock*/ 144: 69, /*a*/ 65: 30, /*b*/ 66: 48, /*c*/ 67: 46, /*d*/ 68: 32, /*e*/ 69: 18, /*f*/ 70: 33, /*g*/ 71: 34, /*h*/ 72: 35, /*i*/ 73: 23, /*j*/ 74: 36, /*k*/ 75: 37, /*l*/ 76: 38, /*m*/ 77: 50, /*n*/ 78: 49, /*o*/ 79: 24, /*p*/ 80: 25, /*q*/ 81: 16, /*r*/ 82: 19, /*s*/ 83: 31, /*t*/ 84: 20, /*u*/ 85: 22, /*v*/ 86: 47, /*w*/ 87: 17, /*x*/ 88: 45, /*y*/ 89: 21, /*z*/ 90: 44, /*1*/ 49: 2, /*2*/ 50: 3, /*3*/ 51: 4, /*4*/ 52: 5, /*5*/ 53: 6, /*6*/ 54: 7, /*7*/ 55: 8, /*8*/ 56: 9, /*9*/ 57: 10, /*0*/ 48: 11, /*; (firefox)*/ 59: 39, /*; (chrome)*/ 186: 39, /*=(firefox)*/ 61: 13, /*=(chrome)*/ 187: 13, /*,*/ 188: 51, /*-(minus in firefox)*/ 173: 12, /*-(dash in chrome)*/ 189: 12, /*.*/ 190: 52, /*/*/ 191: 53, /*`*/ 192: 41, /*{*/ 219: 26, /*\*/ 220: 43, /*}*/ 221: 27, /*'*/ 222: 40, /*left-arrow*/ 37: 105, /*up-arrow*/ 38: 103, /*right-arrow*/ 39: 106, /*down-arrow*/ 40: 108, /*Insert*/ 45: 110, /*Delete*/ 46: 111, /*Home*/ 36: 102, /*End*/ 35: 107, /*Page Up*/ 33: 104, /*Page Down*/ 34: 109, /*F1 */ 112: 59, /*F2 */ 113: 60, /*F3 */ 114: 61, /*F4 */ 115: 62, /*F5 */ 116: 63, /*F6 */ 117: 64, /*F7 */ 118: 65, /*F8 */ 119: 66, /*F9 */ 120: 67, /*F10 */ 121: 68, /*F11 */ 122: 87, /*F12 */ 123: 88, /*. Del*/ 110: 83, /*0 Ins*/ 96: 82, /*1 End*/ 97: 79, /*2 down-arrow*/ 98: 80, /*3 Pg Dn*/ 99: 81, /*4 left-arrow*/ 100: 75, /*5*/ 101: 76, /*6 right-arrow*/ 102: 77, /*7 Home*/ 103: 71, /*8 up-arrow*/ 104: 72, /*9 Pg Up*/ 105: 73, /*+*/ 107: 78, /*-*/ 109: 74, /***/ 106: 55, /*/*/ 111: 98, /*Keypad Enter*/ 13: 28 }; var convertedKeys = []; arr.forEach(function (a) { if (map[a] !== undefined) convertedKeys.push(map[a]); //else // convertedKeys.push(a); }); return convertedKeys; } function convertCharCode(ch) { var arr = []; if (ch >= 48 && ch <= 57) { /* 0..9 */ arr[0] = ch; arr = convertKeycodes(arr); } else if (ch >= 97 && ch <= 122) { /* a..z */ arr[0] = ch - 32; arr = convertKeycodes(arr); } else if (ch >= 65 && ch <= 90) { /* A..Z */ arr[0] = 16; arr[1] = ch; arr = convertKeycodes(arr); } else if (ch == 46) { // . arr[0] = 52; } else if (ch == 33) { // ! arr[0] = 42; arr[1] = 2; } else if (ch == 63) { // ? arr[0] = 42; arr[1] = 53; } else if (ch == 44) { // , arr[0] = 51; } else if (ch == 34) { // " arr[0] = 42; arr[1] = 40; } else if (ch == 39) { // ' arr[0] = 40; } else if (ch == 58) { // : arr[0] = 42; arr[1] = 39; } else if (ch == 40) { // ( arr[0] = 42; arr[1] = 10; } else if (ch == 41) { // ) arr[0] = 42; arr[1] = 11; } else if (ch == 126) { // ~ arr[0] = 42; arr[1] = 41; } else if (ch == 42) { // * arr[0] = 42; arr[1] = 9; } else if (ch == 45) { // - arr[0] = 12; } else if (ch == 47) { // / arr[0] = 53; } else if (ch == 64) { // @ arr[0] = 42; arr[1] = 3; } else if (ch == 95) { // _ arr[0] = 42; arr[1] = 12; } return arr; } function toKeyCode() { var getCharCode = function (str) { return str.charCodeAt(str.length - 1); }; var cc = getCharCode(this.value); document.getElementById("datamessage").removeEventListener("keyup", toKeyCode); this.value = ""; var keysArray = convertCharCode(cc); if (datachannel && document.getElementById('keypresssend').checked && keysArray.length) { var keycodes = { keycodes: keysArray }; datachannel.send(JSON.stringify(keycodes)); } } ; function keydown(e) { if (e.keyCode == 0 || e.keyCode == 229) { // on mobile return; } e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); keys[e.keyCode] = e.keyCode; for (var i = keys.length; i >= 0; i--) { if (keys[i] !== 16 && keys[i] !== 17 && keys[i] !== 18 && keys[i] !== 225 && keys[i] !== e.keyCode) keys[i] = false; } var keysArray = convertKeycodes(getKeycodesArray(keys)); if (datachannel && document.getElementById('keypresssend').checked && keysArray.length) { var keycodes = { keycodes: keysArray }; datachannel.send(JSON.stringify(keycodes)); } } ; function keyup(e) { if (e.keyCode == 0 || e.keyCode == 229) { // on mobile document.getElementById("datamessage").addEventListener("keyup", toKeyCode); return; } e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); keys[e.keyCode] = false; } ; function keypresssend_selection() { if (document.getElementById('keypresssend').checked) { window.addEventListener('keydown', keydown, true); window.addEventListener('keyup', keyup, true); } else { keys = []; window.removeEventListener('keydown', keydown, true); window.removeEventListener('keyup', keyup, true); } } function trickleice_selection() { if (document.getElementById('trickleice').value === "false") { trickle_ice = false; } else if (document.getElementById('trickleice').value === "true") { trickle_ice = true; } else { trickle_ice = null; } return trickle_ice; } window.onload = function () { if (window.MediaRecorder === undefined) { document.getElementById('record').disabled = true; } if (false) { start(); } }; window.onbeforeunload = function () { if (ws) { ws.onclose = function () {}; // disable onclose handler first stop(); } }; </script> <style> #container { display: flex; flex-flow: row nowrap; align-items: flex-end; } video { background: #eee none repeat scroll 0 0; border: 1px solid #aaa; } .overlayWrapper { position: relative; } .overlayWrapper .overlay { position: absolute; top: 0; left: 5px; } p { margin: 0.125em; } small { font-size: smaller; } </style> </head> <body> <h1> <span>WebRTC two-way Audio/Video/Data Intercom & Recorder</span> </h1> <h3 style="color:red" > <span>WARNING! Some browsers do not allow to access local media on insecure origins.</span> <span>Consider switching the UV4L Streaming Server to secure HTTPS instead.</span> </h3> <div id="container"> <div class="overlayWrapper"> <video id="remote-video" autoplay="" width="640" height="480"> Your browser does not support the video tag. </video> <p class="overlay">remote</p> </div> <div class="overlayWrapper"> <video id="local-video" autoplay="" width="320" height="240"> Your browser does not support the video tag. </video> <p class="overlay">local</p> </div> </div> <div id="controls"> <button type=button id="pause" onclick="pause();" title="pause or resume local player">Pause/Resume</button> <button type=button id="mute" onclick="mute();" title="mute or unmute remote audio source">Mute/Unmute</button> <button type=button id="fullscreen" onclick="fullscreen();">Fullscreen</button> <button type=button id="record" onclick="start_stop_record();" title="start or stop recording audio/video">Start Recording</button> </div> <fieldset> <legend><b>Remote peer options</b></legend> <div> <span>Video:</span> <label><input type="checkbox" onclick="remote_hw_vcodec_selection();" id="remote_hw_vcodec" name="remote_hw_vcodec" value="remote_hw_vcodec" title="try to force the use of the hardware codec for both encoding and decoding if enabled and supported">force use of hardware codec for</label> <select id="remote_vformat" name="remote_vformat" onclick="remote_hw_vcodec_format_selection();" title="available resolutions and frame rates at the min., max. and start configured bitrates for adaptive streaming which will be scaled from the base 720p 30fps"> <option value="5">320x240 15 fps</option> <option value="10">320x240 30 fps</option> <option value="20">352x288 30 fps</option> <option value="25">640x480 15 fps</option> <option value="30">640x480 30 fps</option> <option value="35">800x480 30 fps</option> <option value="40">960x720 30 fps</option> <option value="50">1024x768 30 fps</option> <option value="55">1280x720 15 fps</option> <option value="60" selected="selected">1280x720 30 fps, kbps min.800 max.4000 start1200</option> <option value="63">1280x720 60 fps</option> <option value="65">1280x768 15 fps</option> <option value="70">1280x768 30 fps</option> <option value="75">1536x768 30 fps</option> <option value="80">1280x960 30 fps</option> <option value="90">1600x768 30 fps</option> <option value="95">1640x1232 15 fps</option> <option value="97">1640x1232 30 fps</option> <option value="98">1792x896 15 fps</option> <option value="99">1792x896 30 fps</option> <option value="100">1920x1080 15 fps</option> <option value="105">1920x1080 30 fps</option> </select> <p id="note1_"><small>NOTE: if your browser does not support the hardware codec yet, try Firefox with the codec plugin enabled or a recent version of Chrome.</small></p> </div> </fieldset> <div> <details id="record-detail"> <summary><b>Recorded Audio/Video stream</b></summary> <div> <div class="overlayWrapper"> <video id="recorded-video" controls> Your browser does not support the video tag. </video> <p class="overlay">recorded</p> </div> </div> <div> <p><small>NOTE: some old Chrome version may generate corrupted video if the audio track is not present as well (use Firefox in this case)</small></p> <button type=button id="discard" onclick="discard_recording();" title="discard recorded audio/video">Discard</button> <button type=button id="download" onclick="download();" title="save recorded audio/video">Save as</button> </div> </details> </div><br> <fieldset> <legend><b>Cast local Audio/Video sources to remote peer</b></legend> <div> <span>Audio:</span> <label><input type="checkbox" onclick="singleselection('audio_cast', 'cast_mic');" id="cast_mic" name="audio_cast" value="microphone">microphone/other input</label> <label><input type="checkbox" id="echo_cancellation" name="audio_processing" title="disable any audio processing when casting music" checked>echo cancellation</label> <!--label><input type="checkbox" onclick="singleselection('audio_cast', 'cast_tab');" id="cast_tab" name="audio_cast" value="system">tab</label--> </div> <div> <span>Video:</span> <label><input type="checkbox" onclick="singleselection('video_cast', 'cast_camera');" id="cast_camera" name="video_cast" value="camera">camera</label> <label><input type="checkbox" onclick="singleselection('video_cast', 'cast_screen');" id="cast_screen" name="video_cast" value="screen">screen</label> <label><input type="checkbox" onclick="singleselection('video_cast', 'cast_window');" id="cast_window" name="video_cast" value="screen">window</label> <label><input type="checkbox" onclick="singleselection('video_cast', 'cast_application');" id="cast_application" name="video_cast" value="application">application</label> </div> <div> <p id="note1"><small>NOTE: camera and screen can be casted over HTTPS only in Chrome. For the screen the --enable-usermedia-screen-capturing flag must be set. window or application casting is only supported in Firefox 44 on.</small></p> <p id="note2"><small>NOTE: except for camera, to enable screen, window or application casting open <i>about:config</i> URL and set <i>media.getusermedia.screensharing.enabled</i> to <i>true</i> and permanently add the current domain to the list in <i>media.getusermedia.screensharing.allowed_domains.</i></small> </p> <p id="note3"><small>NOTE: if you want to cast music, for better audio quality disable <i>echo-cancellation.</i></small></p> <p id="note4"><small>NOTE: if you want to cast music, for better audio quality disable <i>echo-cancellation</i>, and <i>aec</i>, <i>noise-suppression</i>, <i>agc</i> in the browser configuration <i>(about:config).</i></small> </p> </div> </fieldset> <div> <details> <summary><b>Data Channels</b></summary> <fieldset id="datachannels" disabled> <span>message: </span><input type="text" id="datamessage" value="" title="message to send to the remote peer"/> <button id="datasend" onclick="send_message();">Send</button> <span>received: </span><input type="text" readonly="readonly" id="datareceived" size="40" title="data received from the remote peer"/><br> <label><input type="checkbox" onclick="orientationsend_selection();" id="orientationsend" name="orientationsend" title="send device orientation angles when they change">send device orientation angles alpha, beta, gamma</label> <label><input type="checkbox" onclick="keypresssend_selection();" id="keypresssend" name="keypresssend" title="send keyboard events. Assume US layout. For users with virtual keyboard: put the focus on the 'message' input text item.">send key codes (US layout)</label> <label><input type="checkbox" onclick="alert('not implemented yet');" id="mousesend" name="mousesend" title="send mouse events">send mouse events</label> </fieldset> <!--fieldset id="localdatachannels"> <button id="datacreate" onclick="create_localdatachannel();">Create</button> <button id="dataclose" onclick="close_localdatachannel();">Close</button> </fieldset--> </details> </div><br> <div id="commands"> <details open> <summary><b>Advanced options</b></summary> <fieldset> <span>Remote Peer/Signalling Server Address: </span><input required type="text" id="signalling_server" value="192.168.3.3:8090" title="<host>:<port>, default address is autodetected"/><br> <span>Optional ICE Servers (STUN/TURN): </span><input type="text" id="ice_servers" value="" title="array of RTCIceServer objects as valid JSON string"/><br> <span>Trickle ICE: </span> <select onclick="trickleice_selection();" id="trickleice" name="trickleice" title="enable trickle ice"> <option value="auto">auto</option> <option value="true" selected="selected">true</option> <option value="false">false</option> </select> </fieldset> </details> <button id="start" style="background-color: green; color: white" onclick="start();">Call!</button> <button disabled id="stop" style="background-color: red; color: white" onclick="stop();">Hang up</button> </div><br> <a target="_top" href="/">home</a>&nbsp;<a href="/panel" target="_blank" title="change the image settings on-the-fly">control panel</a> </body> </html>

次回はこのコードを読んで行きたいと思います。

SINCE 2026