-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
387 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Прототип локального прокторинга | ||
|
||
## Инструкция | ||
|
||
В браузере Google Chrome открыть расширения и в режиме разработчика нажать кнопку Load unpacked (загрузить распакованное), в открывшемся меню выбрать папку client. | ||
|
||
Для запуска сервера, в папке server введите следующую команду: | ||
```bash | ||
docker-compose up --build | ||
``` | ||
|
||
Для получения видео с сервера можно использовать следующую команду: | ||
```bash | ||
curl -O http://localhost:5000/get/<file_id> | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
chrome.action.onClicked.addListener(() => { | ||
chrome.tabs.create({ url: chrome.runtime.getURL('index.html') }); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<link rel="stylesheet" href="style.css"> | ||
</head> | ||
<body class="page"> | ||
<h1>Локальный прокторинг</h1> | ||
<input type="text" name="name" id="username_input" placeholder="Введите имя" required minlength="2" maxlength="40"> | ||
<div class="record-section"> | ||
<button class="save-location_button">Выбрать место сохранения</button> | ||
<button class="permissions_button">Разрешения</button> | ||
<button class="upload_button">Отправить</button> | ||
<span class="upload_info"></span> | ||
</div> | ||
<div class="record-section"> | ||
<button class="record-start_button">Начать запись</button> | ||
<button class="record-stop_button" disabled>Остановить запись</button> | ||
</div> | ||
<video class="output-video" controls autoplay ></video> | ||
<!--<video class="camera" width="320" height="240"></video> | ||
<video class="screen" width="800" height="600"></video> | ||
<audio class="microphone" src=""></audio>--> | ||
|
||
|
||
<script src="index.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
const saveLocationButton = document.querySelector('.save-location_button'); | ||
const startRecordButton = document.querySelector('.record-start_button'); | ||
const stopRecordButton = document.querySelector('.record-stop_button'); | ||
const permissionsButton = document.querySelector('.permissions_button'); | ||
const uploadButton = document.querySelector('.upload_button'); | ||
const uploadInfo = document.querySelector('.upload_info'); | ||
const usernameInput = document.querySelector('#username_input'); | ||
const outputVideo = document.querySelector('.output-video'); | ||
//const cameraSelector = document.querySelector('.camera'); | ||
//const screenSelector = document.querySelector('.screen'); | ||
//const audioSelector = document.querySelector('.microphone'); | ||
|
||
let directoryHandle = null; | ||
let fileHandler = null; | ||
let recorder = null; | ||
let cancel = false; | ||
let startRecordTime = null; | ||
let finishRecordTime = null; | ||
|
||
const getCurrentDateString = (date) => { | ||
return `${date.getDate()}-${date.getMonth()+1}-${date.getFullYear()}T${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`; | ||
} | ||
|
||
async function getMedia() { | ||
if (!directoryHandle) { | ||
uploadInfo.textContent = "Выберите место сохранения"; | ||
return; | ||
} | ||
try { | ||
// facingMode: "user" - для получения фронтальной камеры | ||
//const cameraStream = await navigator.mediaDevices.getUserMedia({video: {facingMode: "user"} }); | ||
|
||
const screenStream = await navigator.mediaDevices.getDisplayMedia({video: true}); | ||
const audioStream = await navigator.mediaDevices.getUserMedia({audio: true}); | ||
|
||
const audioTrack = audioStream.getAudioTracks()[0]; | ||
const videoTrack = screenStream.getVideoTracks()[0]; | ||
|
||
const combinedStream = new MediaStream([videoTrack, audioTrack]); | ||
|
||
outputVideo.srcObject = combinedStream; | ||
|
||
outputVideo.onloadedmetadata = function() { | ||
outputVideo.width = outputVideo.videoWidth > 800 ? 800 : outputVideo.videoWidth; | ||
outputVideo.height = outputVideo.videoHeight > 600 ? 600 : outputVideo.videoHeight; | ||
}; | ||
|
||
|
||
videoTrack.onended = function() { | ||
uploadInfo.textContent = "Демонстрация экрана была прекращена. Пожалуйста, перезапустите запись."; | ||
cancel = true; | ||
}; | ||
|
||
audioTrack.onended = function() { | ||
uploadInfo.textContent = "Разрешение на микрофон было сброшено. Пожалуйста, разрешите микрофон для продолжения."; | ||
cancel = true; | ||
}; | ||
|
||
// Для записи создаем новый MediaRecorder | ||
recorder = new MediaRecorder(combinedStream, {mimeType: "video/webm"}); | ||
|
||
// Получаем путь для сохранения файла | ||
|
||
const writableStream = await fileHandler.createWritable(); | ||
recorder.ondataavailable = async (event) => { | ||
if (event.data.size > 0) { | ||
await writableStream.write(event.data); | ||
} | ||
}; | ||
|
||
recorder.onstop = async () => { | ||
await writableStream.close(); | ||
console.log("Запись завершена и файл сохранён локально."); | ||
//if (cameraStream) { | ||
// cameraStream.getTracks().forEach(track => track.stop()); | ||
//} | ||
|
||
if (screenStream) { | ||
screenStream.getTracks().forEach(track => track.stop()); | ||
} | ||
|
||
if (audioStream) { | ||
audioStream.getTracks().forEach(track => track.stop()); | ||
} | ||
|
||
//if (canvasStream) { | ||
// canvasStream.getTracks().forEach(track => track.stop()); | ||
//} | ||
|
||
if (combinedStream) { | ||
combinedStream.getTracks().forEach(track => track.stop()); | ||
} | ||
|
||
outputVideo.srcObject = null; | ||
|
||
console.log("Все потоки и запись остановлены."); | ||
}; | ||
|
||
} catch(err) { | ||
console.log(err); | ||
} | ||
} | ||
|
||
async function startRecordCallback() { | ||
if (directoryHandle === null) { | ||
uploadInfo.textContent = "Выберите место сохранения"; | ||
return; | ||
} | ||
if (!outputVideo.srcObject) { | ||
uploadInfo.textContent = "Выдайте разрешения"; | ||
return; | ||
} | ||
uploadInfo.textContent = ""; | ||
startRecordButton.setAttribute('disabled', ''); | ||
stopRecordButton.removeAttribute('disabled'); | ||
recorder.start(); | ||
} | ||
|
||
function stopRecordCallback() { | ||
stopRecordButton.setAttribute('disabled', ''); | ||
startRecordButton.removeAttribute('disabled'); | ||
finishRecordTime = getCurrentDateString(new Date()); | ||
recorder.stop(); | ||
} | ||
|
||
function getPermissionsCallback() { | ||
cancel = false; | ||
uploadButton.classList.remove('upload_button_fail'); | ||
uploadButton.classList.remove('upload_button_success'); | ||
uploadInfo.textContent = ""; | ||
getMedia(); | ||
} | ||
|
||
startRecordButton.addEventListener('click', startRecordCallback); | ||
|
||
stopRecordButton.addEventListener('click', stopRecordCallback) | ||
|
||
permissionsButton.addEventListener('click', getPermissionsCallback); | ||
|
||
saveLocationButton.addEventListener('click', async () => { | ||
directoryHandle = await window.showDirectoryPicker(); | ||
startRecordTime = getCurrentDateString(new Date()); | ||
fileHandler = await directoryHandle.getFileHandle(`proctoring_${startRecordTime}`, {create: true}); | ||
console.log(directoryHandle); | ||
}); | ||
|
||
uploadButton.addEventListener('click', async () => { | ||
console.log("Отправка..."); | ||
if (usernameInput.value === '') { | ||
uploadInfo.textContent = `Введите имя в поле!`; | ||
uploadButton.classList.add('upload_button_fail'); | ||
return; | ||
} | ||
if (!fileHandler || cancel) { | ||
uploadInfo.textContent = `Записи не было или сбросили разрешение2!`; | ||
uploadButton.classList.add('upload_button_fail'); | ||
return; | ||
} | ||
if (recorder.state !== 'inactive') { | ||
uploadInfo.textContent = `Остановите запись!`; | ||
uploadButton.classList.add('upload_button_fail'); | ||
return; | ||
} | ||
const file = await fileHandler.getFile(); | ||
if (!file) { | ||
uploadInfo.textContent = `Файл не найден!`; | ||
uploadButton.classList.add('upload_button_fail'); | ||
return; | ||
} | ||
uploadInfo.textContent = ""; | ||
const username = usernameInput.value; | ||
const formData = new FormData(); | ||
formData.append('file', file); | ||
formData.append('username', username); | ||
formData.append('start', startRecordTime); | ||
formData.append('end', finishRecordTime); | ||
|
||
|
||
fetch('http://127.0.0.1:5000/upload', { | ||
method: 'POST', | ||
mode: 'cors', | ||
body: formData, | ||
}) | ||
.then(res => { | ||
if (res.ok) { | ||
return res.json(); | ||
} | ||
return Promise.reject(`Ошибка при загрузке файла: ${res.status}`); | ||
}) | ||
.then(result => { | ||
uploadInfo.textContent = `Файл успешно загружен, ID: ${result.file_id}`; | ||
uploadButton.classList.remove('upload_button_fail'); | ||
uploadButton.classList.add('upload_button_success'); | ||
}) | ||
.catch(err => { | ||
uploadInfo.textContent = err; | ||
uploadButton.classList.remove('upload_button_success'); | ||
uploadButton.classList.add('upload_button_fail'); | ||
}) | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"name": "Local proctoring", | ||
"description": "Local screencast", | ||
"version": "0.1", | ||
"manifest_version": 3, | ||
"permissions": [], | ||
"action": { | ||
|
||
}, | ||
"background": { | ||
"service_worker": "background.js" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
.page { | ||
margin: 0; | ||
padding: 5px; | ||
border: 1px solid black; | ||
overflow: hidden; | ||
} | ||
|
||
.record-section { | ||
display: flex; | ||
flex-direction: row; | ||
gap: 8px; | ||
} | ||
|
||
.upload_button_success { | ||
background-color: green; | ||
} | ||
|
||
.upload_button_fail { | ||
background-color: red; | ||
} | ||
|
||
.output-video { | ||
margin: 0; | ||
box-sizing: content-box; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
FROM python:3.9-slim | ||
|
||
WORKDIR /app | ||
|
||
COPY . /app | ||
|
||
RUN pip install --no-cache-dir -r requirements.txt | ||
|
||
EXPOSE 5000 | ||
|
||
CMD ["python", "app.py"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from flask import Flask, request, jsonify | ||
from pymongo import MongoClient | ||
import gridfs | ||
from bson import ObjectId | ||
from flask_cors import CORS | ||
|
||
app = Flask(__name__) | ||
CORS(app, resources={r"/*": {"origins": "*"}}) | ||
|
||
client = MongoClient('mongodb://mongo-db:27017/') | ||
db = client["video_database"] | ||
fs = gridfs.GridFS(db) # Используем GridFS для работы с файлами | ||
|
||
def parse_time(time: str): | ||
# 24-2-2025T12-51-55 | ||
time = time.split('T') | ||
date = list(map(int, time[0].split('-'))) | ||
time = list(map(int, time[1].split('-'))) | ||
date_time = { | ||
'D': date[0], | ||
'M': date[1], | ||
'Y': date[2], | ||
'h': time[0], | ||
'm': time[1], | ||
's': time[2] | ||
} | ||
return date_time | ||
|
||
|
||
|
||
@app.route('/upload', methods=['POST']) | ||
def upload_file(): | ||
file = request.files['file'] | ||
username = request.form['username'] | ||
start = parse_time(request.form['start']) | ||
end = parse_time(request.form['end']) | ||
file_id = fs.put(file.read(), filename=file.filename, metadata={ | ||
'username': username, 'start': start, 'end': end}) | ||
return jsonify({"file_id": str(file_id)}), 200 | ||
|
||
@app.route('/get/<file_id>', methods=['GET']) | ||
def get_file(file_id): | ||
try: | ||
file_data = fs.get(ObjectId(file_id)) | ||
return file_data.read(), 200, { | ||
'Content-Type': 'video/webm', | ||
'Content-Disposition': f'attachment; filename={file_data.filename}' | ||
} | ||
except gridfs.errors.NoFile: | ||
return jsonify({"error": "File not found"}), 404 | ||
|
||
if __name__ == '__main__': | ||
app.run(host='0.0.0.0', port=5000, debug=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
version: '3.8' | ||
|
||
services: | ||
# Сервис для Flask приложения | ||
flask-app: | ||
build: . | ||
container_name: flask-app | ||
ports: | ||
- "5000:5000" | ||
depends_on: | ||
- mongo-db | ||
environment: | ||
- FLASK_APP=app.py | ||
- FLASK_ENV=development | ||
volumes: | ||
- .:/app | ||
networks: | ||
- app-network | ||
|
||
# Сервис для MongoDB | ||
mongo-db: | ||
image: mongo:8.0 | ||
container_name: mongo-db | ||
volumes: | ||
- mongo-data:/data/db | ||
networks: | ||
- app-network | ||
|
||
volumes: | ||
mongo-data: | ||
|
||
networks: | ||
app-network: | ||
driver: bridge |
Oops, something went wrong.