Skip to content

Commit

Permalink
Basics #2 PR #3
Browse files Browse the repository at this point in the history
  • Loading branch information
proitshnik authored Feb 25, 2025
2 parents d060476 + a7c6b87 commit bc23ee5
Show file tree
Hide file tree
Showing 10 changed files with 387 additions and 0 deletions.
16 changes: 16 additions & 0 deletions playground/Zazulya/README.md
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>
```

3 changes: 3 additions & 0 deletions playground/Zazulya/client/background.js
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') });
});
28 changes: 28 additions & 0 deletions playground/Zazulya/client/index.html
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>
200 changes: 200 additions & 0 deletions playground/Zazulya/client/index.js
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');
})
});
13 changes: 13 additions & 0 deletions playground/Zazulya/client/manifest.json
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"
}
}
25 changes: 25 additions & 0 deletions playground/Zazulya/client/style.css
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;
}
11 changes: 11 additions & 0 deletions playground/Zazulya/server/Dockerfile
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"]
53 changes: 53 additions & 0 deletions playground/Zazulya/server/app.py
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)
34 changes: 34 additions & 0 deletions playground/Zazulya/server/docker-compose.yml
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
Loading

0 comments on commit bc23ee5

Please sign in to comment.