Video Sharing (Upload + Watch Page, No DB)

February 14, 2026 NEW

A tiny “video sharing” script you can drop on shared hosting. It lets people upload videos, generates a clean watch page, and shows a simple “latest videos” feed — all without a database.

What it does:

  • Upload videos (mp4, webm, mov).
  • Stores metadata as JSON files (title, filename, created date, views).
  • Creates a watch page via ?v=slug (SEO-friendly-ish without rewrites).
  • Counts views (simple “hit counter” per video).
  • Optional: if FFmpeg exists, auto-generates a thumbnail (falls back gracefully).

Install:

  1. Create: /tools/video-share/
  2. Create two writable folders inside it:
    • /tools/video-share/_v/ (video files)
    • /tools/video-share/_db/ (metadata JSON)
    • /tools/video-share/_thumbs/ (optional thumbs)
  3. Save the script below as: /tools/video-share/index.php
  4. Visit: /tools/video-share/

Note: This is intentionally minimal. Real public video sites need serious moderation, storage, and abuse controls. This is best for small communities, private sharing, or “demo” use.

<?php
declare(strict_types=1);

/**
 * Tiny Video Sharing (No DB) — One File
 * /tools/video-share/index.php
 *
 * - Upload video + title
 * - Stores JSON metadata per video
 * - Lists latest + watch pages
 * - Counts views
 * - Optional FFmpeg thumbnail generation (if available)
 */

header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');

$CFG = [
  'site_name' => 'Your Site',
  'tool_name' => 'Tiny Video Share',
  'desc'      => 'Lightweight video sharing: upload, browse, and watch. No database.',
  'canonical' => 'https://your-site.com/tools/video-share/', // change

  // Storage (must be writable)
  'dir_v'      => __DIR__ . '/_v',
  'dir_db'     => __DIR__ . '/_db',
  'dir_thumbs' => __DIR__ . '/_thumbs',

  // Upload limits
  'max_mb'     => 80, // adjust to your host limits
  'allowed'    => ['mp4','webm','mov'],

  // Anti-spam
  'honeypot'   => 'companyfax',
  'rate_hits'  => 3,      // uploads
  'rate_window'=> 1800,   // per 30 min per IP

  // FFmpeg thumbnail (optional)
  'ffmpeg_path' => 'ffmpeg', // or full path; leave as 'ffmpeg'
  'thumb_seconds'=> 2,
];

session_start();
function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); }

function ensure_dir(string $dir): bool { return is_dir($dir) || @mkdir($dir, 0755, true); }
function ip(): string { return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; }

function rl_path(string $key): string {
  return sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'vs_rl_' . sha1($key) . '.json';
}
function rate_limit(string $scope, int $maxHits, int $window): array {
  $bucket = (int)floor(time() / max(1, $window));
  $key = $scope . '|' . ip() . '|' . $bucket;
  $path = rl_path($key);

  $fh = @fopen($path, 'c+');
  if (!$fh) return [true, 0];
  if (!flock($fh, LOCK_EX)) { fclose($fh); return [true, 0]; }

  $raw = '';
  $sz = @filesize($path);
  if ($sz && $sz < 4096) { rewind($fh); $raw = (string)fread($fh, $sz); }

  $st = ['reset' => time() + $window, 'count' => 0];
  if ($raw !== '') {
    $j = json_decode($raw, true);
    if (is_array($j) && isset($j['reset'],$j['count'])) { $st['reset']=(int)$j['reset']; $st['count']=(int)$j['count']; }
  }
  if (time() >= $st['reset']) { $st['reset']=time()+$window; $st['count']=0; }

  $st['count']++;
  $ok = ($st['count'] <= $maxHits);
  $retry = max(1, $st['reset'] - time());

  rewind($fh); ftruncate($fh, 0);
  fwrite($fh, json_encode($st, JSON_UNESCAPED_SLASHES));
  fflush($fh);

  flock($fh, LOCK_UN);
  fclose($fh);

  return [$ok, $retry];
}

function csrf_token(): string {
  if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(16));
  return (string)$_SESSION['csrf'];
}
function csrf_ok(string $t): bool {
  return isset($_SESSION['csrf']) && hash_equals((string)$_SESSION['csrf'], $t);
}

function slugify(string $s): string {
  $s = strtolower(trim($s));
  $s = preg_replace('/[^a-z0-9\s-]/', '', $s) ?? $s;
  $s = preg_replace('/\s+/', '-', $s) ?? $s;
  $s = preg_replace('/-+/', '-', $s) ?? $s;
  $s = trim($s, '-');
  return $s !== '' ? $s : 'video';
}

function json_read(string $file): array {
  if (!is_file($file)) return [];
  $raw = @file_get_contents($file);
  if ($raw === false) return [];
  $j = json_decode($raw, true);
  return is_array($j) ? $j : [];
}
function json_write(string $file, array $data): bool {
  $out = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
  if ($out === false) return false;
  return @file_put_contents($file, $out, LOCK_EX) !== false;
}

function list_videos(string $dirDb): array {
  $out = [];
  if (!is_dir($dirDb)) return $out;
  foreach (glob($dirDb . '/*.json') ?: [] as $file) {
    $j = json_read($file);
    if (!$j) continue;
    $out[] = $j;
  }
  usort($out, function($a,$b){
    return strcmp((string)($b['createdAt'] ?? ''), (string)($a['createdAt'] ?? ''));
  });
  return $out;
}

function thumb_for(array $v): string {
  $t = (string)($v['thumb'] ?? '');
  if ($t !== '') return $t;
  return '';
}

function try_make_thumb(array $CFG, string $videoPath, string $thumbPath): bool {
  // generate a JPG thumbnail using ffmpeg (if available)
  $ff = trim((string)$CFG['ffmpeg_path']);
  if ($ff === '') return false;

  $sec = max(0, (int)$CFG['thumb_seconds']);
  $cmd = escapeshellcmd($ff)
    . ' -y -ss ' . escapeshellarg((string)$sec)
    . ' -i ' . escapeshellarg($videoPath)
    . ' -vframes 1 -q:v 4 '
    . escapeshellarg($thumbPath)
    . ' 2>/dev/null';

  @exec($cmd, $o, $code);
  return $code === 0 && is_file($thumbPath) && filesize($thumbPath) > 1000;
}

// ensure dirs
ensure_dir($CFG['dir_v']);
ensure_dir($CFG['dir_db']);
ensure_dir($CFG['dir_thumbs']);

$notice = '';
$error  = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  if (!csrf_ok((string)($_POST['csrf'] ?? ''))) {
    $error = 'Security check failed. Refresh and try again.';
  } else {
    [$ok, $retry] = rate_limit('video_upload', (int)$CFG['rate_hits'], (int)$CFG['rate_window']);
    if (!$ok) $error = 'Rate limited. Try again in ' . $retry . 's.';

    $hp = (string)($_POST[(string)$CFG['honeypot']] ?? '');
    if ($error === '' && $hp !== '') $error = 'Spam blocked.';

    $title = trim((string)($_POST['title'] ?? ''));
    if ($error === '' && (mb_strlen($title) < 3 || mb_strlen($title) > 120)) $error = 'Title must be 3–120 chars.';

    $f = $_FILES['video'] ?? null;
    if ($error === '' && (!is_array($f) || (int)($f['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK)) {
      $error = 'Upload failed.';
    }

    if ($error === '' && is_array($f)) {
      $sz = (int)($f['size'] ?? 0);
      $maxBytes = (int)$CFG['max_mb'] * 1024 * 1024;
      if ($sz <= 0 || $sz > $maxBytes) $error = 'File too large (max ' . (int)$CFG['max_mb'] . 'MB).';

      $name = (string)($f['name'] ?? '');
      $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
      if ($error === '' && ($ext === '' || !in_array($ext, (array)$CFG['allowed'], true))) {
        $error = 'Unsupported file type.';
      }

      if ($error === '') {
        $base = slugify($title);
        $id = gmdate('Ymd_His') . '_' . bin2hex(random_bytes(6));
        $slug = $base . '-' . substr($id, -6);

        $fileName = $slug . '.' . $ext;
        $destVideo = rtrim($CFG['dir_v'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $fileName;

        if (!@move_uploaded_file((string)$f['tmp_name'], $destVideo)) {
          $error = 'Could not store video (permissions?).';
        } else {
          $thumbName = '';
          $thumbRel  = '';
          $thumbFile = rtrim($CFG['dir_thumbs'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $slug . '.jpg';

          if (try_make_thumb($CFG, $destVideo, $thumbFile)) {
            $thumbName = basename($thumbFile);
            $thumbRel  = '_thumbs/' . $thumbName;
          }

          $meta = [
            'id'        => $id,
            'slug'      => $slug,
            'title'     => $title,
            'file'      => '_v/' . $fileName,
            'thumb'     => $thumbRel,
            'views'     => 0,
            'createdAt' => gmdate('c'),
          ];

          $metaFile = rtrim($CFG['dir_db'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $slug . '.json';
          json_write($metaFile, $meta);

          header('Location: ?v=' . rawurlencode($slug));
          exit;
        }
      }
    }
  }
}

// route
$slug = (string)($_GET['v'] ?? '');
$slug = preg_replace('/[^a-z0-9\-]/', '', strtolower($slug)) ?? '';
$video = [];
if ($slug !== '') {
  $video = json_read(rtrim($CFG['dir_db'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $slug . '.json');
  if ($video) {
    // increment views (simple)
    $video['views'] = (int)($video['views'] ?? 0) + 1;
    json_write(rtrim($CFG['dir_db'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $slug . '.json', $video);
  }
}

$all = list_videos($CFG['dir_db']);

function current_url_base(): string {
  $proto = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
  $host = $_SERVER['HTTP_HOST'] ?? '';
  $path = $_SERVER['SCRIPT_NAME'] ?? '';
  return $proto . $host . $path;
}

$pageTitle = $video ? ((string)$video['title'] . ' — ' . $CFG['tool_name']) : ($CFG['tool_name']);
$desc = $CFG['desc'];
$canon = $video ? (current_url_base() . '?v=' . rawurlencode((string)$video['slug'])) : (string)$CFG['canonical'];

?><!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title><?php echo h($pageTitle); ?></title>
  <meta name="description" content="<?php echo h($desc); ?>" />
  <link rel="canonical" href="<?php echo h($canon); ?>" />
  <meta name="robots" content="index,follow,max-snippet:-1,max-image-preview:large,max-video-preview:-1" />

  <style>
    :root{
      --bg:#0b0f16; --panel:#111a26; --panel2:#0c1420;
      --text:#e8eef8; --muted:rgba(232,238,248,.72);
      --line:rgba(255,255,255,.10); --accent:#2a8cff;
      --shadow:0 18px 55px rgba(0,0,0,.45); --r:16px;
    }
    *{box-sizing:border-box}
    body{margin:0;background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
    a{color:#8ad1ff;text-decoration:none}
    a:hover{text-decoration:underline}
    .wrap{max-width:1050px;margin:0 auto;padding:18px}
    .card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:14px;margin:12px 0;box-shadow:var(--shadow)}
    .muted{color:var(--muted)}
    .top{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;align-items:center}
    .pill{display:inline-block;padding:4px 10px;border-radius:999px;background:rgba(42,140,255,.14);border:1px solid rgba(42,140,255,.25);font-weight:900;font-size:12px}
    .btn{display:inline-flex;align-items:center;gap:8px;border:0;border-radius:12px;padding:10px 12px;background:var(--accent);color:#06101a;font-weight:900;cursor:pointer}
    input,textarea{
      width:100%;padding:10px 12px;border-radius:12px;border:1px solid var(--line);
      background:var(--panel2);color:var(--text);outline:none
    }
    .grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:12px}
    .c7{grid-column:span 7}
    .c5{grid-column:span 5}
    @media(max-width:980px){.c7,.c5{grid-column:span 12}}
    .thumb{
      width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:14px;
      border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.04)
    }
    .vwrap{
      border-radius:16px;border:1px solid rgba(255,255,255,.10);overflow:hidden;background:#000
    }
    video{display:block;width:100%;height:auto;max-height:70vh}
    .hr{height:1px;background:var(--line);margin:12px 0}
    .item{display:grid;grid-template-columns:180px 1fr;gap:12px;align-items:center}
    @media(max-width:720px){.item{grid-template-columns:1fr}}
  </style>
</head>
<body>
<div class="wrap">

  <div class="card">
    <div class="top">
      <div>
        <div class="pill">No DB</div>
        <h1 style="margin:10px 0 6px;font-size:22px"><?php echo h((string)$CFG['tool_name']); ?></h1>
        <div class="muted"><?php echo h((string)$CFG['desc']); ?></div>
      </div>
      <div style="display:flex;gap:10px;flex-wrap:wrap">
        <a class="btn" href="./">🏠 Home</a>
        <a class="btn" href="#upload">⬆️ Upload</a>
      </div>
    </div>
    <?php if ($error): ?><div class="hr"></div><div class="muted" style="color:#ffb3b3"><b><?php echo h($error); ?></b></div><?php endif; ?>
    <?php if ($notice): ?><div class="hr"></div><div class="muted"><b><?php echo h($notice); ?></b></div><?php endif; ?>
  </div>

  <?php if ($video): ?>
    <div class="card">
      <h2 style="margin:0 0 8px"><?php echo h((string)$video['title']); ?></h2>
      <div class="muted" style="font-size:13px">Views: <b><?php echo (int)($video['views'] ?? 0); ?></b> • Uploaded: <?php echo h((string)($video['createdAt'] ?? '')); ?></div>
      <div class="hr"></div>
      <div class="vwrap">
        <video controls playsinline preload="metadata">
          <source src="<?php echo h((string)$video['file']); ?>" type="<?php
            $ext = strtolower(pathinfo((string)$video['file'], PATHINFO_EXTENSION));
            echo $ext === 'webm' ? 'video/webm' : 'video/mp4';
          ?>">
          Your browser does not support the video tag.
        </video>
      </div>
      <div class="muted" style="margin-top:10px;font-size:13px">
        Share link: <code><?php echo h(current_url_base() . '?v=' . (string)$video['slug']); ?></code>
      </div>
    </div>
  <?php endif; ?>

  <div class="grid">
    <div class="card c7">
      <h2 style="margin:0 0 10px;font-size:18px">Latest videos</h2>

      <?php if (!$all): ?>
        <div class="muted">No uploads yet.</div>
      <?php else: ?>
        <?php foreach (array_slice($all, 0, 20) as $v): ?>
          <div class="item" style="margin:12px 0">
            <?php
              $t = thumb_for($v);
              if ($t !== '' && is_file(__DIR__ . '/' . $t)) {
                echo '<a href="?v='.h((string)$v['slug']).'"><img class="thumb" src="'.h($t).'" alt=""></a>';
              } else {
                echo '<a href="?v='.h((string)$v['slug']).'"><div class="thumb" style="display:flex;align-items:center;justify-content:center;color:rgba(232,238,248,.55);font-weight:900">No thumbnail</div></a>';
              }
            ?>
            <div>
              <div style="font-weight:900">
                <a href="?v=<?php echo h((string)$v['slug']); ?>"><?php echo h((string)$v['title']); ?></a>
              </div>
              <div class="muted" style="font-size:13px;margin-top:4px">
                <?php echo h((string)($v['createdAt'] ?? '')); ?> • Views: <?php echo (int)($v['views'] ?? 0); ?>
              </div>
            </div>
          </div>
          <div class="hr"></div>
        <?php endforeach; ?>
      <?php endif; ?>
    </div>

    <div class="card c5" id="upload">
      <h2 style="margin:0 0 8px;font-size:18px">Upload a video</h2>
      <div class="muted" style="font-size:13px;line-height:1.4">
        Allowed: <b><?php echo h(implode(', ', (array)$CFG['allowed'])); ?></b> • Max: <b><?php echo (int)$CFG['max_mb']; ?>MB</b><br>
        This is a tiny demo—use moderation if you open it publicly.
      </div>

      <form method="post" enctype="multipart/form-data" style="margin-top:12px;display:grid;gap:10px">
        <input type="hidden" name="csrf" value="<?php echo h(csrf_token()); ?>">
        <div>
          <div class="muted" style="font-size:12px;margin-bottom:6px">Title</div>
          <input name="title" required maxlength="120" placeholder="Short title..." />
        </div>
        <div>
          <div class="muted" style="font-size:12px;margin-bottom:6px">Video file</div>
          <input type="file" name="video" required accept="video/*" />
        </div>

        <!-- honeypot -->
        <div style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden">
          <label>Leave blank <input name="<?php echo h((string)$CFG['honeypot']); ?>" autocomplete="off"></label>
        </div>

        <button class="btn" type="submit">⬆️ Upload</button>
      </form>
    </div>
  </div>

  <div class="card">
    <div class="muted" style="font-size:13px;line-height:1.5">
      <b>Want to expand it?</b>
      <ul style="margin:8px 0 0; padding-left:18px">
        <li>Add an approval queue before videos go public.</li>
        <li>Limit uploads by IP/day or require a “share token”.</li>
        <li>Generate smaller transcoded MP4s (FFmpeg) for compatibility.</li>
      </ul>
    </div>
  </div>

</div>
</body>
</html>

Comments (0)

No comments yet — be the first.

← Back to all scripts