Webbserverprogrammering 1

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)
// @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;
  }
});