Show sourcecode
The following files exists in this folder. Click to view.
Webserver1/Ovningar/Slutprojekt/
.env
DEBUG/
Media/
account.js
account.php
callback_log.txt
change_account_details.php
composer.json
composer.lock
forgot_pass.php
forgot_pass_new_pass.php
header.php
index.php
login.php
mediaplayer.php
node_modules/
package-lock.json
package.json
signup.php
style.css
upload.js
upload_callback.php
upload_callback_simulated.php
upload_chunk.php
upload_errors.log
upload_form.php
upload_handler.php
upload_success.log
vendor/
verify_file.php
verifypage.php
upload.js
342 lines UTF-8 Windows (CRLF)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
// @ts-check
import { FFmpeg } from './node_modules/@ffmpeg/ffmpeg/dist/esm/index.js';
const SERVER_UPLOAD_MAX = 2 * 1024 * 1024; // 2 MB server upload limit
const MAX_CHUNK_SIZE = 1.5 * 1024 * 1024; // 1.5 MB per chunk to stay under server limit
const MAX_VIDEO_SIZE = 200 * 1024 * 1024; // 200 MB
const MAX_IMAGE_SIZE = 2 * 1024 * 1024; // 2 MB
const AUDIO_BITRATE = 96_000; // 96 kbps
const MIN_VIDEO_BITRATE = 400_000;
// Absoluta max längd med konstant MIN_VIDEO_BITRATE, med 5% marginal
const maxDuration = 0.95 * (MAX_VIDEO_SIZE * 8) / (MIN_VIDEO_BITRATE + AUDIO_BITRATE);
/**@type {File | null} */
let compressedFile;
let upload_ready = false;
// Informations element
const uploadProgressElement = /**@type {HTMLProgressElement} */(
document.getElementById("upload-progress")
);
const progressLabel = /**@type {HTMLLabelElement} */(
document.getElementById("progress-label")
);
const progressSpan = /**@type {HTMLSpanElement} */ (
document.getElementById("progress-pct")
);
const infoSpan = /**@type {HTMLSpanElement} */ (
document.getElementById("info-span")
);
const preUploadInfo = /**@type {HTMLDivElement} */ (
document.getElementById("pre-upload-info")
);
const sizeWarning = /**@type {HTMLParagraphElement} */ (
document.getElementById("size-warning-message")
);
// Input element
const videoInput = /**@type {HTMLInputElement} */ (
document.getElementById("video-input")
);
const thumbInput = /**@type {HTMLInputElement} */ (
document.getElementById("thumb-input")
);
const thumbErrorElement = document.createElement("div");
thumbErrorElement.id = "thumb-error";
thumbErrorElement.style.color = "red";
thumbErrorElement.style.display = "none";
thumbInput.parentElement?.firstElementChild?.appendChild(thumbErrorElement);
const videoTempIdInput = /**@type {HTMLInputElement} */ (
document.getElementById("video-temp-id")
);
const isSeriesCheckBox = /**@type {HTMLInputElement} */ (
document.getElementById("is-series")
);
const form = /**@type {HTMLFormElement} */ (
document.getElementById("upload-form")
);
const ffmpeg = new FFmpeg();
let isLoaded = false; // Flagga för om ffmpeg är laddat
async function ensureLoaded() {
if (!isLoaded) {
console.log("Loading FFmpeg...");
await ffmpeg.load();
isLoaded = true;
console.log("FFmpeg loaded successfully.");
}
}
/** @param {File} file */
function generateUploadId(file) {
if (crypto && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return `upload-${Date.now()}-${Math.floor(Math.random() * 1e9)}`;
}
/** @param {File} file */
async function uploadVideoInChunks(file) {
const uploadId = generateUploadId(file);
const totalChunks = Math.ceil(file.size / MAX_CHUNK_SIZE);
uploadProgressElement.max = totalChunks;
uploadProgressElement.value = 0;
progressLabel.style.display = 'block';
uploadProgressElement.style.display = 'inline-block';
progressSpan.style.display = 'inline';
sizeWarning.hidden = true;
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const start = chunkIndex * MAX_CHUNK_SIZE;
const end = Math.min(file.size, start + MAX_CHUNK_SIZE);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('uploadId', uploadId);
formData.append('chunkIndex', String(chunkIndex));
formData.append('totalChunks', String(totalChunks));
formData.append('fileName', file.name);
formData.append('fileType', file.type);
formData.append('fileSize', String(file.size));
formData.append('chunk', chunk, file.name);
infoSpan.innerHTML = `Laddar upp del ${chunkIndex + 1} av ${totalChunks}...`;
console.log(`Uploading chunk ${chunkIndex + 1}/${totalChunks}`);
const response = await fetch('./upload_chunk.php', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`Chunk upload request failed: ${response.status}`);
}
const result = await response.json();
if (result.status !== 'ok') {
throw new Error(`Chunk upload failed: ${result.message || 'Unknown error'}`);
}
uploadProgressElement.value = chunkIndex + 1;
progressSpan.innerHTML = (((chunkIndex + 1) / totalChunks) * 100).toFixed(1) + '%';
}
return uploadId;
}
console.log(videoInput);
// Komprimera video när den laddats upp
videoInput.addEventListener("change", async () => {
progressLabel.style.display = "block"
uploadProgressElement.style.display = "inline-block";
progressSpan.style.display = "inline";
sizeWarning.hidden = true;
// console.log("Upload ändrat!");
const file = videoInput.files?.[0];
// Reset temporary chunk state if the user picks a new file
videoTempIdInput.value = '';
videoInput.name = 'video-input';
// Kolla om vi har en fil
if (!file) return;
// Kolla om filen är en video
if (!file.type.startsWith("video/")) {
alert("Inkorrekt filformat, du måste ladda upp en video!");
return;
}
preUploadInfo.classList.add("hidden");
if (file.size > SERVER_UPLOAD_MAX) {
console.log(`File size ${file.size} > ${SERVER_UPLOAD_MAX}, starting chunked upload`);
try {
const uploadId = await uploadVideoInChunks(file);
videoTempIdInput.value = uploadId;
videoInput.removeAttribute('name');
upload_ready = true;
infoSpan.innerHTML = "Videon är uppladdad i chunkar. Fyll i metadata och tryck Ladda upp.";
console.log("Chunked upload complete, temp id:", uploadId);
} catch (err) {
console.error(err);
const message = err instanceof Error ? err.message : String(err);
alert("Chunkuppladdningen misslyckades: " + message);
upload_ready = false;
}
return;
}
// Se till att ffmpeg är laddat
infoSpan.innerHTML = "Laddar FFmpeg...";
await ensureLoaded();
let data;
try {
data = await file.arrayBuffer();
}
catch (err) {
console.error("Filen " + file.name + " kunde inte läsas. Error: " + err);
alert("Ett fel uppstod när filen skulle läsas. Troligen är filen du försökte ladda upp för stor. (Chrome max: 2GB, Safari max: 4GB, Firefox max: 8GB)");
return;
}
// Ladda filen till ffmpegs minne
console.log("Loading file into memory...");
infoSpan.innerHTML = "Laddar fil till minne...";
await ffmpeg.writeFile(file.name, new Uint8Array(data));
console.log("File loaded.");
ffmpeg.on("progress", ({progress, time}) => {
console.log("progress:", progress, "time:", time);
uploadProgressElement.value = progress;
progressSpan.innerHTML = (progress * 100).toFixed(1) + "%";
})
ffmpeg.on("log", ({ type, message }) => {
console.log("LOG:", "type:", type, "message:", message);
})
// Skriv duration
await ffmpeg.ffprobe([
"-v", "error", // Skriv bara ut errors
"-show_entries", "format=duration", // Visa bara duration
"-of", "default=noprint_wrappers=1:nokey=1", // Skriv inga wrappers, skriv inga nycklar
file.name, // Filen användaren skickade
"-o", "output.txt" // Spara i output.txt
]);
const output = await ffmpeg.readFile('output.txt')
// @ts-ignore
console.log(new TextDecoder().decode(output));
// @ts-ignore
const duration = Number(new TextDecoder().decode(output));
if (duration > maxDuration) {
alert("Videon är för lång! Maxlängd är " + Math.floor(maxDuration / 60).toString());
return;
}
let ffmpegArgs = [];
if (file.size > MAX_VIDEO_SIZE) {
// Target med 15% säkerhetsmarginal
const targetBitrate = Math.max(
400_000,
(MAX_VIDEO_SIZE * 8 / duration - AUDIO_BITRATE) * 0.85
);
sizeWarning.hidden = false;
ffmpegArgs = [
"-i", file.name, // Input är filen användaren skickade
// Video
"-c:v", "libx264", // Använd h.264 encoding för video
"-preset", "ultrafast", // ultrafast kompression preset
"-tune", "fastdecode", // Snabbare decoding
"-b:v", Math.floor(targetBitrate).toString(), // Bitrate som bör få videon till ca MAX_VIDEO_SIZE storlek
"-maxrate", Math.floor(targetBitrate).toString(), // Maximala tillåtna bitrate
"-bufsize", Math.floor(2 * targetBitrate).toString(), // Bufferstorlek för decoder (update varannan sek)
"-vf", "scale=1280:720", // Skala video till 720p
// Audio
"-c:a", "aac", // Använd aac encoding för ljud
"-b:a", "96k", // Bitrate på 96k för ljud (standard)
// Optimering för mp4
"-movflags", "+faststart",
"output.mp4" // Spara som output.mp4
]
console.log("Compressing video...");
infoSpan.innerHTML = "Komprimerar video...";
await ffmpeg.exec(ffmpegArgs);
console.log("Compression finished!");
// Hämta fil från WASM minne
const outputData = await ffmpeg.readFile('output.mp4');
// @ts-ignore
const blob = new Blob([outputData], {type: "video/mp4"});
compressedFile = new File([blob], 'compressed.mp4',
{type: "video/mp4"}
)
upload_ready = true;
}
else {
console.log("Skipping compression...");
infoSpan.innerHTML = "Skippar komprimering...";
upload_ready = true;
compressedFile = null;
}
console.log("Video ready for upload!");
infoSpan.innerHTML = "Videon är redo att laddas upp!";
uploadProgressElement.style.display = "none";
progressSpan.style.display = "none";
});
// Validera thumbnail storlek när den väljs
thumbInput.addEventListener("change", () => {
const file = thumbInput.files?.[0];
if (!file) {
thumbErrorElement.style.display = "none";
return;
}
if (file.size > MAX_IMAGE_SIZE) {
thumbErrorElement.textContent = "Thumbnail är för stor! Max är 2 MB. Vald fil är " + (file.size / (1024 * 1024)).toFixed(2) + " MB.";
thumbErrorElement.style.display = "block";
} else {
thumbErrorElement.style.display = "none";
}
});
// // Toggle kontroller för serie-relaterad info
// isSeriesCheckBox.addEventListener("change", () => {
// console.log("Checkbox changed!")
// const seriesElements = document.querySelectorAll(".series-info")
// seriesElements.forEach((element) => {
// console.log(element)
// element.classList.toggle("hidden")
// })
// })
// Byt ut videoinput till compressed fil
form.addEventListener('submit', async (e) => {
if (!upload_ready) {
e.preventDefault(); // Stoppar default submit
alert("Videon är inte färdig än!");
return;
}
if (!thumbInput.files?.[0]) {
e.preventDefault();
alert("Invalid eller ingen thumbnail fil!");
return;
}
if (thumbInput.files[0].size > MAX_IMAGE_SIZE) {
e.preventDefault();
alert("Thumbnail fil är för stor! Max är 2 MB!");
return;
}
if (videoTempIdInput.value) {
videoInput.removeAttribute('name');
} else if (compressedFile) {
const dt = new DataTransfer();
dt.items.add(compressedFile);
videoInput.files = dt.files;
}
});