Lots of ui work
@ -1,7 +1,8 @@
|
|||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
from flask import Flask, redirect, url_for, request, session, make_response
|
import base64
|
||||||
|
from flask import Flask, redirect, url_for, request, session, make_response, jsonify
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
@ -13,6 +14,9 @@ import uuid
|
|||||||
|
|
||||||
random_order = True
|
random_order = True
|
||||||
|
|
||||||
|
# activate environment: cd C:\Users\Jan\Google Drive\Master Stuff\Code\SLAEForms Testing\.venv\Scripts\
|
||||||
|
# then this: activate
|
||||||
|
|
||||||
#create the app
|
#create the app
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
# configure the database, give it a path (it will be in the instances folder)
|
# configure the database, give it a path (it will be in the instances folder)
|
||||||
@ -58,12 +62,21 @@ with app.app_context():
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/video", methods=["GET", "POST"])
|
@app.route("/video", methods=["GET", "POST"])
|
||||||
def sendpage():
|
def videopage():
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"layout2.html"
|
#"videorecorder3.html"
|
||||||
|
"myvideotemplate.html"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.route("/send_video", methods=["POST"])
|
||||||
|
def send_video():
|
||||||
|
data_url = request.json['dataUrl']
|
||||||
|
data = data_url.split(',')[1]
|
||||||
|
with open('video.webm', 'wb') as f:
|
||||||
|
f.write(base64.b64decode(data))
|
||||||
|
return jsonify({'message': 'Video saved successfully'})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/send", methods=["POST"])
|
@app.route("/send", methods=["POST"])
|
||||||
@ -83,6 +96,8 @@ def sendpage():
|
|||||||
return redirect("/form")
|
return redirect("/form")
|
||||||
except:
|
except:
|
||||||
return "There was a problem while adding the response to the Database"
|
return "There was a problem while adding the response to the Database"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/form", methods=["GET", "POST"]) # /<username> should not even be needed right?
|
@app.route("/form", methods=["GET", "POST"]) # /<username> should not even be needed right?
|
||||||
def formpage():
|
def formpage():
|
||||||
|
@ -1,3 +1,54 @@
|
|||||||
https://jamesalvarez.co.uk/blog/how-to-make-responsive-likert-scales-in-css-(like-qualtrics)/
|
https://jamesalvarez.co.uk/blog/how-to-make-responsive-likert-scales-in-css-(like-qualtrics)/
|
||||||
|
|
||||||
|
|
||||||
|
Licenses:
|
||||||
|
|
||||||
|
Open Iconic Icon Set (https://github.com/iconic/open-iconic)
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Waybury
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
Bootstrap Icons https://github.com/twbs/icons
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2019-2024 The Bootstrap Authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
BIN
slaeforms/static/icons/camera-icon.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
slaeforms/static/icons/camera-off-icon.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
slaeforms/static/icons/camera-reels-icon.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
slaeforms/static/icons/check-icon.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
slaeforms/static/icons/record-icon.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
slaeforms/static/icons/stop-icon.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
slaeforms/static/icons/trash-icon.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
slaeforms/static/icons/x-icon.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
@ -8,6 +8,22 @@ body {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.videocontrols {
|
||||||
|
width: 100px; /* Set a specific width for the buttons */
|
||||||
|
height: 70px; /* Set a specific height for the buttons */
|
||||||
|
background-color: #cae4ff;
|
||||||
|
border: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
margin: 0 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
display: inline-flex; /* Display button contents as a flexbox */
|
||||||
|
justify-content: center; /* Center contents horizontally */
|
||||||
|
align-items: center; /* Center contents vertically */
|
||||||
|
}
|
||||||
|
|
||||||
.columncontainer {
|
.columncontainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
0
slaeforms/static/videoscript.js
Normal file
@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="de">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
139
slaeforms/templates/myvideotemplate.html
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css')}}" />
|
||||||
|
<!-- styles.css {{ url_for('static', filename='styles.css')}}-->
|
||||||
|
<title>Testform</title>
|
||||||
|
<style>
|
||||||
|
/* Add any styling here */
|
||||||
|
.videocontrols {
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videocontrols img {
|
||||||
|
max-width: 60%;
|
||||||
|
max-height: 60%;
|
||||||
|
width: auto;
|
||||||
|
/* Make the image fill its container */
|
||||||
|
height: auto;
|
||||||
|
/* Make the image fill its container */
|
||||||
|
display: block;
|
||||||
|
/* Remove any extra space around the image */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Gib Feedback als Video</h2>
|
||||||
|
<div class="centertext">
|
||||||
|
|
||||||
|
<button type="button" class="videocontrols" id="buttonCamera">
|
||||||
|
<img id="buttonCameraIcon" src="{{ url_for('static', filename='icons/camera-icon.png')}}" alt="Camera Icon"
|
||||||
|
onclick="cameraButton()">
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="centertext">
|
||||||
|
|
||||||
|
<button type="button" class="videocontrols" id="buttonRecord" style="display:none">
|
||||||
|
<img id="buttonRecordIcon" src="{{ url_for('static', filename='icons/record-icon.png')}}" alt="Camera Icon"
|
||||||
|
onclick="recordButton()">
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="videocontrols" id="buttonAccept" style="display:none">
|
||||||
|
<img id="buttonAcceptIcon" src="{{ url_for('static', filename='icons/check-icon.png')}}" alt="Accept Icon"
|
||||||
|
onclick="acceptButton()">
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="videocontrols" id="buttonCancel" style="display:none">
|
||||||
|
<img id="buttonCancelIcon" src="{{ url_for('static', filename='icons/x-icon.png')}}" alt="Cancel Icon"
|
||||||
|
onclick="cancelButton()">
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="videocontrols" id="buttonDelete" style="display:none">
|
||||||
|
<img id="buttonDeleteIcon" src="{{ url_for('static', filename='icons/trash-icon.png')}}" alt="Delete Icon"
|
||||||
|
onclick="deleteButton()">
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<video autoplay muted playsinline width="1280" height="720" id="videoDisplay"></video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const buttonCamera = document.getElementById('buttonCamera');
|
||||||
|
const buttonRecord = document.getElementById('buttonRecord');
|
||||||
|
const buttonAccept = document.getElementById('buttonAccept');
|
||||||
|
const buttonCancel = document.getElementById('buttonCancel');
|
||||||
|
const buttonDelete = document.getElementById('buttonRecord');
|
||||||
|
const videoDisplay = document.getElementById('videoDisplay');
|
||||||
|
const buttonCameraIcon = document.getElementById('buttonCameraIcon');
|
||||||
|
const buttonRecordIcon = document.getElementById('buttonRecordIcon');
|
||||||
|
var mediaRecorder = null;
|
||||||
|
var stream = null;
|
||||||
|
let recordedVideoBlob = null;
|
||||||
|
let isRecording = false;
|
||||||
|
let videoAccess = false;
|
||||||
|
|
||||||
|
async function cameraButton() {
|
||||||
|
if (!videoAccess) { //TODO what happens if you dont get the device
|
||||||
|
console.log("videoAccess = false");
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: true,
|
||||||
|
});
|
||||||
|
console.log("stream is active");
|
||||||
|
videoAccess = true
|
||||||
|
videoDisplay.srcObject = stream
|
||||||
|
buttonCameraIcon.src = "{{ url_for('static', filename='icons/camera-off-icon.png') }}"; //todo, not sure if this works
|
||||||
|
buttonCameraIcon.alt = "Camera-off Icon";
|
||||||
|
|
||||||
|
buttonRecord.style.display = 'inline-block';
|
||||||
|
} else {
|
||||||
|
console.log("videoAccess = true");
|
||||||
|
stream = null;
|
||||||
|
videoAccess = false
|
||||||
|
buttonCameraIcon.src = "{{ url_for('static', filename='icons/camera-icon.png') }}"; //todo, not sure if this works
|
||||||
|
buttonCameraIcon.alt = "Camera Icon";
|
||||||
|
buttonRecord.style.display = 'none';
|
||||||
|
}
|
||||||
|
console.log("camera button function ends");
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordButton() {
|
||||||
|
if (!isRecording) {
|
||||||
|
mediaRecorder = new MediaRecorder(stream, {
|
||||||
|
mimeType: "video/webm", //could use other video format: https://www.iana.org/assignments/media-types/media-types.xhtml#video
|
||||||
|
// I probably want to change the bitrate: https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/MediaRecorder
|
||||||
|
});
|
||||||
|
isRecording = true;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoAccess)
|
||||||
|
|
||||||
|
buttonRecording.addEventListener("click", function () {
|
||||||
|
if (isRecording) {
|
||||||
|
recordingIcon.src = 'record-icon.png';
|
||||||
|
recordingIcon.alt = 'record icon';
|
||||||
|
console.log('Action started');
|
||||||
|
} else {
|
||||||
|
recordingIcon.src = 'stop-icon.png';
|
||||||
|
recordingIcon.alt = 'Stop Icon';
|
||||||
|
console.log('Action stopped');
|
||||||
|
}
|
||||||
|
isRecording = !isRecording;
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
65
slaeforms/templates/videorecorder.html
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Webcam Recorder</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<video id="video" width="640" height="480" autoplay></video>
|
||||||
|
<button id="startRecord">Start Recording</button>
|
||||||
|
<button id="stopRecord">Stop Recording</button>
|
||||||
|
<button id="save">Save</button>
|
||||||
|
<canvas id="canvas" style="display: none;"></canvas>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let video = document.getElementById('video');
|
||||||
|
let startRecordBtn = document.getElementById('startRecord');
|
||||||
|
let stopRecordBtn = document.getElementById('stopRecord');
|
||||||
|
let saveBtn = document.getElementById('save');
|
||||||
|
let canvas = document.getElementById('canvas');
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
let stream;
|
||||||
|
|
||||||
|
navigator.mediaDevices.getUserMedia({ video: true })
|
||||||
|
.then(function (mediaStream) {
|
||||||
|
video.srcObject = mediaStream;
|
||||||
|
stream = mediaStream;
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.error('Could not access the webcam: ', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
startRecordBtn.onclick = function () {
|
||||||
|
console.log('Recording testprint')
|
||||||
|
stream.getVideoTracks()[0].requestRecord({ mimeType: 'video/webm' })
|
||||||
|
.then(() => console.log('Recording started'))
|
||||||
|
.catch(err => console.error('Failed to start recording: ', err));
|
||||||
|
};
|
||||||
|
|
||||||
|
stopRecordBtn.onclick = function () {
|
||||||
|
stream.getVideoTracks()[0].stopRecord()
|
||||||
|
.then(blob => {
|
||||||
|
console.log('Recording stopped');
|
||||||
|
let videoURL = URL.createObjectURL(blob);
|
||||||
|
video.src = videoURL;
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Failed to stop recording: ', err));
|
||||||
|
};
|
||||||
|
|
||||||
|
saveBtn.onclick = function () {
|
||||||
|
let data = video.toDataURL('image/png');
|
||||||
|
fetch('/send_video', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ image: data })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => console.log('Video saved successfully:', data))
|
||||||
|
.catch(error => console.error('Error saving video:', error));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
84
slaeforms/templates/videorecorder2.html
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<!-- index.html -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Webcam Recorder</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<video id="video" width="640" height="480" autoplay></video>
|
||||||
|
<button id="startRecord">Start Recording</button>
|
||||||
|
<button id="stopRecord">Stop Recording</button>
|
||||||
|
<button id="save">Save</button>
|
||||||
|
<canvas id="canvas" style="display: none;"></canvas>
|
||||||
|
<div id="messageArea"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let video = document.getElementById('video');
|
||||||
|
let startRecordBtn = document.getElementById('startRecord');
|
||||||
|
let stopRecordBtn = document.getElementById('stopRecord');
|
||||||
|
let saveBtn = document.getElementById('save');
|
||||||
|
let canvas = document.getElementById('canvas');
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
let stream;
|
||||||
|
let messageArea = document.getElementById('messageArea');
|
||||||
|
showMessage('Start', 'success')
|
||||||
|
navigator.mediaDevices.getUserMedia({ video: true })
|
||||||
|
.then(function (mediaStream) {
|
||||||
|
video.srcObject = mediaStream;
|
||||||
|
stream = mediaStream;
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
showMessage('Could not access the webcam: ' + err, 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
startRecordBtn.onclick = function () {
|
||||||
|
showMessage('Recording started', 'success')
|
||||||
|
stream.getVideoTracks()[0].requestRecord({ mimeType: 'video/webm' })
|
||||||
|
.then(() => showMessage('Recording started', 'success'))
|
||||||
|
.catch(err => showMessage('Failed to start recording: ' + err, 'error'));
|
||||||
|
};
|
||||||
|
|
||||||
|
stopRecordBtn.onclick = function () {
|
||||||
|
stream.getVideoTracks()[0].stopRecord()
|
||||||
|
.then(blob => {
|
||||||
|
showMessage('Recording stopped', 'success');
|
||||||
|
let videoURL = URL.createObjectURL(blob);
|
||||||
|
video.src = videoURL;
|
||||||
|
})
|
||||||
|
.catch(err => showMessage('Failed to stop recording: ' + err, 'error'));
|
||||||
|
};
|
||||||
|
|
||||||
|
saveBtn.onclick = function () {
|
||||||
|
let data = canvas.toDataURL('image/png');
|
||||||
|
fetch('/send_video', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ image: data })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => showMessage('Video saved successfully', 'success'))
|
||||||
|
.catch(error => showMessage('Error saving video: ' + error, 'error'));
|
||||||
|
};
|
||||||
|
|
||||||
|
function showMessage(message, type) {
|
||||||
|
let messageDiv = document.createElement('div');
|
||||||
|
messageDiv.textContent = message;
|
||||||
|
messageDiv.classList.add(type);
|
||||||
|
messageArea.appendChild(messageDiv);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.success {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
</html>
|
177
slaeforms/templates/videorecorder3.html
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>
|
||||||
|
Videorecorder test
|
||||||
|
</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Videorecordertest</h1>
|
||||||
|
<div>
|
||||||
|
<button type="button" id="buttonStart">Start</button>
|
||||||
|
<button type="button" id="buttonStop" disabled>Stop</button>
|
||||||
|
<button type="button" id="buttonUploadBlob" disabled>
|
||||||
|
Upload (Blob)
|
||||||
|
</button>
|
||||||
|
<button type="button" id="buttonUploadDataURL" disabled>
|
||||||
|
Upload (DataURL)
|
||||||
|
</button>
|
||||||
|
<div id="messageArea"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<video autoplay muted playsinline width="1280" height="720" id="videoLive"></video>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<video controls playsinline width="1280" height="720" id="videoRecorded"></video>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function showMessage(message, type) {
|
||||||
|
let messageDiv = document.createElement('div');
|
||||||
|
messageDiv.textContent = message;
|
||||||
|
messageDiv.classList.add(type);
|
||||||
|
messageArea.appendChild(messageDiv);
|
||||||
|
}
|
||||||
|
async function main() {
|
||||||
|
const buttonStart = document.querySelector("#buttonStart");
|
||||||
|
const buttonStop = document.querySelector("#buttonStop");
|
||||||
|
const buttonUploadBlob = document.querySelector("#buttonUploadBlob");
|
||||||
|
const buttonUploadDataURL = document.querySelector(
|
||||||
|
"#buttonUploadDataURL"
|
||||||
|
);
|
||||||
|
const videoLive = document.querySelector("#videoLive");
|
||||||
|
const videoRecorded = document.querySelector("#videoRecorded");
|
||||||
|
let recordedVideoBlob = null;
|
||||||
|
let messageArea = document.getElementById('messageArea');
|
||||||
|
const search = new URLSearchParams({
|
||||||
|
extname: ".webm",
|
||||||
|
}).toString();
|
||||||
|
// this string is literally jsut "extname=.webm", WHY?
|
||||||
|
showMessage('const search:', 'success')
|
||||||
|
showMessage(search, 'success')
|
||||||
|
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
videoLive.srcObject = stream;
|
||||||
|
|
||||||
|
const mediaRecorder = new MediaRecorder(stream, {
|
||||||
|
mimeType: "video/webm", //could use other video format: https://www.iana.org/assignments/media-types/media-types.xhtml#video
|
||||||
|
// I probably want to influence the bitrate: https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/MediaRecorder
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonStart.addEventListener("click", () => {
|
||||||
|
|
||||||
|
mediaRecorder.start();
|
||||||
|
buttonStart.setAttribute("disabled", "");
|
||||||
|
buttonStop.removeAttribute("disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonStop.addEventListener("click", () => {
|
||||||
|
mediaRecorder.stop();
|
||||||
|
buttonStart.removeAttribute("disabled");
|
||||||
|
buttonStop.setAttribute("disabled", "");
|
||||||
|
buttonUploadBlob.removeAttribute("disabled");
|
||||||
|
buttonUploadDataURL.removeAttribute("disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
mediaRecorder.addEventListener("dataavailable", (event) => {
|
||||||
|
videoRecorded.src = URL.createObjectURL(event.data);
|
||||||
|
recordedVideoBlob = event.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonUploadBlob.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
const search = new URLSearchParams({
|
||||||
|
extname: ".webm",
|
||||||
|
}).toString();
|
||||||
|
// this string is literally jsut "extname=.webm", WHY?
|
||||||
|
|
||||||
|
const url = "/api/upload/blob?" + search;
|
||||||
|
|
||||||
|
console.log(recordedVideoBlob);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
},
|
||||||
|
body: recordedVideoBlob,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 201) {
|
||||||
|
console.warn(response.status);
|
||||||
|
console.warn(await response.text());
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonUploadDataURL.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
const url = "/send_video";
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=UTF-8",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
extname: ".webm",
|
||||||
|
dataUrl: await convertBlob(recordedVideoBlob),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 201) {
|
||||||
|
console.warn(response.status);
|
||||||
|
console.warn(await response.text());
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertBlob(blob) {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
|
||||||
|
const subscribe = () => {
|
||||||
|
fileReader.addEventListener("abort", onAbort);
|
||||||
|
fileReader.addEventListener("error", onError);
|
||||||
|
fileReader.addEventListener("load", onLoad);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsubscribe = () => {
|
||||||
|
fileReader.removeEventListener("abort", onAbort);
|
||||||
|
fileReader.removeEventListener("error", onError);
|
||||||
|
fileReader.removeEventListener("load", onLoad);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAbort = () => {
|
||||||
|
unsubscribe();
|
||||||
|
reject(new Error("abort"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onError = (event) => {
|
||||||
|
unsubscribe();
|
||||||
|
reject(event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLoad = (event) => {
|
||||||
|
unsubscribe();
|
||||||
|
resolve(event.target.result);
|
||||||
|
};
|
||||||
|
|
||||||
|
subscribe();
|
||||||
|
fileReader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|