Video Sharing (Upload + Watch Page, No DB)
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:
- Create:
/tools/video-share/ - Create two writable folders inside it:
/tools/video-share/_v/(video files)/tools/video-share/_db/(metadata JSON)/tools/video-share/_thumbs/(optional thumbs)
- Save the script below as:
/tools/video-share/index.php - 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.