feat(live): polish — ended-state, chat, viewer actions, Private option
Restores the live streaming controllers / views / model from the
pre-audit WIP that got stashed on 2026-10-21, then applies the Phase 6
polish items:
## /live/new form
Add a Private visibility option that mirrors the upload modal's
wording — "Only you can see this stream. Perfect for testing without
an audience." Validator in LiveStreamController@store now permits
public|unlisted|private (was public|unlisted).
## Watch page ended-state
When the stream status is "ended", the overlay is a solid "Stream
ended" card showing the stream's duration (started_at → ended_at)
and, when a recording was published, a Watch Replay CTA linking to
the saved video. Previously an ended stream showed the generic
"Waiting for the broadcast to start" placeholder forever.
## Channel name link
The uploader's name in the meta row is now a real <a href> to the
channel page.
## Viewer action row
Subscribe + Share buttons below the player, visible to anyone except
the owner. Subscribe for guests uses the Phase 7 ?intended= pattern
so after sign-in they land back on the stream.
## Live chat (DB-backed polling)
New live_chat_messages table (migration 2026_10_21_000003). Viewers
GET /live/{stream}/chat?since=N every 3 seconds; signed-in users POST
to the same path (rate-limited at 60/min). Guests see the panel in
read-only mode with a "Sign in to chat" placeholder.
Private streams reject chat for anyone but the owner (404). Non-live
status rejects chat sends (422). Author name is snapshotted at post
time so a renamed account's old messages keep the name they used.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
639279f2a2
commit
5bf1128196
12 files changed
+2103
No files matched your search
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Live streaming.
|
||||
*
|
||||
* Purely additive: one new table. No existing table is altered — the link to a
|
||||
* recording is held here as video_id rather than adding a column to `videos`,
|
||||
* so the music and generic video paths run against exactly the schema they run
|
||||
* against today.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('livestreams', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
|
||||
// Public, unpredictable identifier. Used in the watch URL AND as the
|
||||
// media path, so it must never be guessable or enumerable — knowing
|
||||
// one stream's id must not hint at another's.
|
||||
$table->string('public_id', 32)->unique();
|
||||
|
||||
$table->string('title');
|
||||
$table->text('description')->nullable();
|
||||
|
||||
// idle → live → ended, or failed.
|
||||
$table->string('status', 16)->default('idle');
|
||||
|
||||
// public = listed on the live index
|
||||
// unlisted = reachable only with the link
|
||||
$table->string('visibility', 16)->default('public');
|
||||
|
||||
// Short-lived, single-use credential the browser presents to the
|
||||
// media server when it starts publishing. Not a long-lived stream
|
||||
// key: it is minted on "Go live" and expires in minutes, so there
|
||||
// is no durable secret to leak, screenshot, or reuse.
|
||||
$table->string('publish_token', 64)->nullable();
|
||||
$table->timestamp('publish_token_expires_at')->nullable();
|
||||
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('ended_at')->nullable();
|
||||
|
||||
// Last time the media server confirmed the publisher is still
|
||||
// connected — how a stream that died without saying goodbye gets
|
||||
// reaped.
|
||||
$table->timestamp('last_seen_at')->nullable();
|
||||
|
||||
$table->unsignedInteger('peak_viewers')->default(0);
|
||||
$table->unsignedInteger('current_viewers')->default(0);
|
||||
|
||||
// Set once a finished stream has been turned into a normal video.
|
||||
$table->foreignId('video_id')->nullable()->constrained()->nullOnDelete();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// The live index reads "public streams that are live, newest first".
|
||||
$table->index(['status', 'visibility']);
|
||||
$table->index(['user_id', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('livestreams');
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user