207 lines
8.0 KiB
PHP
207 lines
8.0 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Broadcast;
|
|
use App\Models\Category;
|
|
use App\Models\Channel;
|
|
use App\Services\Streaming\StreamKeyManager;
|
|
use App\Services\Streaming\ViewerCountStore;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\Rule;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
class CreatorDashboardController extends Controller
|
|
{
|
|
public function show(Request $request, StreamKeyManager $streamKeys): Response
|
|
{
|
|
$channel = $request->user()
|
|
->channel()
|
|
->with(['category', 'liveBroadcast', 'streamKey'])
|
|
->first();
|
|
|
|
return Inertia::render('dashboard', [
|
|
'channel' => $channel ? $this->channelPayload($channel) : null,
|
|
'canCreateChannel' => $request->user()->canCreateChannel(),
|
|
'plainStreamKey' => session('plain_stream_key'),
|
|
'streaming' => [
|
|
'ingestServer' => $streamKeys->ingestServer(),
|
|
'tokenizedPath' => $channel && session('plain_stream_key')
|
|
? $streamKeys->tokenizedPath($channel, (string) session('plain_stream_key'))
|
|
: null,
|
|
],
|
|
'categories' => Category::query()
|
|
->orderBy('name')
|
|
->get(['id', 'name', 'slug'])
|
|
->map(fn (Category $category) => [
|
|
'id' => $category->id,
|
|
'name' => $category->name,
|
|
'slug' => $category->slug,
|
|
]),
|
|
'recentBroadcasts' => $channel
|
|
? $channel->broadcasts()->with('vod')->latest()->limit(10)->get()->map(fn (Broadcast $broadcast) => [
|
|
'id' => $broadcast->id,
|
|
'title' => $broadcast->title,
|
|
'status' => $broadcast->status,
|
|
'recording_enabled' => $broadcast->recording_enabled,
|
|
'started_at' => $broadcast->started_at?->toIso8601String(),
|
|
'ended_at' => $broadcast->ended_at?->toIso8601String(),
|
|
'has_vod' => $broadcast->vod !== null,
|
|
])
|
|
: [],
|
|
]);
|
|
}
|
|
|
|
public function storeChannel(Request $request, StreamKeyManager $streamKeys): RedirectResponse
|
|
{
|
|
abort_if($request->user()->channel()->exists(), 422, 'This account already owns a channel.');
|
|
abort_unless($request->user()->canCreateChannel(), 403, 'An admin must enable channel creation for this account.');
|
|
|
|
$validated = $request->validate([
|
|
'display_name' => ['required', 'string', 'max:80'],
|
|
'slug' => ['required', 'string', 'min:3', 'max:40', 'alpha_dash:ascii', 'lowercase', 'unique:channels,slug'],
|
|
'description' => ['nullable', 'string', 'max:1000'],
|
|
'category_id' => ['nullable', 'exists:categories,id'],
|
|
]);
|
|
|
|
$channel = $request->user()->channel()->create([
|
|
...$validated,
|
|
'display_name' => trim($validated['display_name']),
|
|
'slug' => Str::slug($validated['slug']),
|
|
]);
|
|
|
|
$plainStreamKey = $streamKeys->rotate($channel);
|
|
|
|
return redirect()
|
|
->route('dashboard')
|
|
->with('plain_stream_key', $plainStreamKey)
|
|
->with('success', 'Channel created. Save the stream key now; it will not be shown again.');
|
|
}
|
|
|
|
public function updateChannel(Request $request): RedirectResponse
|
|
{
|
|
$channel = $request->user()->channel;
|
|
|
|
abort_unless($channel, 404);
|
|
|
|
$validated = $request->validate([
|
|
'display_name' => ['required', 'string', 'max:80'],
|
|
'slug' => ['required', 'string', 'min:3', 'max:40', 'alpha_dash:ascii', 'lowercase', Rule::unique('channels', 'slug')->ignore($channel)],
|
|
'description' => ['nullable', 'string', 'max:1000'],
|
|
'category_id' => ['nullable', 'exists:categories,id'],
|
|
'thumbnail' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:4096'],
|
|
]);
|
|
|
|
if ($request->hasFile('thumbnail')) {
|
|
if ($channel->thumbnail_path) {
|
|
Storage::disk('public')->delete($channel->thumbnail_path);
|
|
}
|
|
|
|
$validated['thumbnail_path'] = $request->file('thumbnail')->store("channels/{$channel->id}/thumbnails", 'public');
|
|
}
|
|
|
|
unset($validated['thumbnail']);
|
|
|
|
$channel->update([
|
|
...$validated,
|
|
'display_name' => trim($validated['display_name']),
|
|
'slug' => Str::slug($validated['slug']),
|
|
]);
|
|
|
|
return back()->with('success', 'Channel updated.');
|
|
}
|
|
|
|
public function rotateStreamKey(Request $request, StreamKeyManager $streamKeys): RedirectResponse
|
|
{
|
|
$channel = $request->user()->channel;
|
|
|
|
abort_unless($channel, 404);
|
|
|
|
$plainStreamKey = $streamKeys->rotate($channel);
|
|
|
|
return back()
|
|
->with('plain_stream_key', $plainStreamKey)
|
|
->with('success', 'Stream key rotated. Update OBS before going live again.');
|
|
}
|
|
|
|
public function storeBroadcast(Request $request): RedirectResponse
|
|
{
|
|
$channel = $request->user()->channel;
|
|
|
|
abort_unless($channel, 404);
|
|
abort_if($channel->isSuspended(), 403);
|
|
abort_if(
|
|
$channel->broadcasts()->whereIn('status', [Broadcast::STATUS_PENDING, Broadcast::STATUS_LIVE])->exists(),
|
|
422,
|
|
'Finish the current broadcast before creating another.',
|
|
);
|
|
|
|
$validated = $request->validate([
|
|
'title' => ['required', 'string', 'max:120'],
|
|
'recording_enabled' => ['nullable', 'boolean'],
|
|
]);
|
|
|
|
$channel->broadcasts()->create([
|
|
'title' => $validated['title'],
|
|
'status' => Broadcast::STATUS_PENDING,
|
|
'recording_enabled' => (bool) ($validated['recording_enabled'] ?? false),
|
|
'mediamtx_path' => $channel->slug,
|
|
]);
|
|
|
|
return back()->with('success', 'Broadcast is ready. Start streaming from OBS.');
|
|
}
|
|
|
|
public function stopBroadcast(Request $request, Broadcast $broadcast, ViewerCountStore $viewerCounts): RedirectResponse
|
|
{
|
|
abort_unless($broadcast->channel->user_id === $request->user()->id, 403);
|
|
|
|
$broadcast->forceFill([
|
|
'status' => Broadcast::STATUS_ENDED,
|
|
'ended_at' => $broadcast->ended_at ?? now(),
|
|
])->save();
|
|
|
|
$broadcast->channel->forceFill([
|
|
'is_live' => false,
|
|
'viewer_count' => 0,
|
|
'live_broadcast_id' => null,
|
|
])->save();
|
|
$viewerCounts->forget($broadcast->channel);
|
|
|
|
return back()->with('success', 'Broadcast ended.');
|
|
}
|
|
|
|
private function channelPayload(Channel $channel): array
|
|
{
|
|
return [
|
|
'id' => $channel->id,
|
|
'slug' => $channel->slug,
|
|
'display_name' => $channel->display_name,
|
|
'description' => $channel->description,
|
|
'avatar_url' => $this->mediaUrl($channel->avatar_path),
|
|
'banner_url' => $this->mediaUrl($channel->banner_path),
|
|
'thumbnail_url' => $this->mediaUrl($channel->thumbnail_path),
|
|
'is_live' => $channel->is_live,
|
|
'viewer_count' => $channel->viewer_count,
|
|
'stream_key_last_used_at' => $channel->streamKey?->last_used_at?->toIso8601String(),
|
|
'category_id' => $channel->category_id,
|
|
'live_broadcast' => $channel->liveBroadcast ? [
|
|
'id' => $channel->liveBroadcast->id,
|
|
'title' => $channel->liveBroadcast->title,
|
|
'status' => $channel->liveBroadcast->status,
|
|
'hls_url' => $channel->liveBroadcast->hls_url,
|
|
'recording_enabled' => $channel->liveBroadcast->recording_enabled,
|
|
'started_at' => $channel->liveBroadcast->started_at?->toIso8601String(),
|
|
] : null,
|
|
];
|
|
}
|
|
|
|
private function mediaUrl(?string $path): ?string
|
|
{
|
|
return $path ? Storage::url($path) : null;
|
|
}
|
|
}
|