Add video platform features: authentication, video management, user profiles, likes/views tracking
- Added authentication controllers (Login, Register) - Added UserController for user profile management - Added VideoController with full CRUD operations - Added Video model with relationships (user, likes, views) - Added User model enhancements (avatar, video relationships) - Added database migrations for video_likes, video_views, user_avatar, video_visibility - Added CompressVideoJob for video processing - Added VideoUploaded mail notification - Added authentication routes - Updated web routes with video and user routes - Added layout templates (app, plain, partials) - Added user views (profile, settings, channel, history, liked) - Added video views (create, edit, index, show) - Added email templates
This commit is contained in:
1 parent
6a2026df4b
commit
5253f89b63
35 files changed
+5092
-849
No files matched your search
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
public function create()
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required'],
|
||||
]);
|
||||
|
||||
if (Auth::attempt($credentials)) {
|
||||
$request->session()->regenerate();
|
||||
return redirect()->intended('/videos');
|
||||
}
|
||||
|
||||
return back()->withErrors([
|
||||
'email' => 'The provided credentials do not match our records.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
return redirect('/videos');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
public function create()
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
||||
'password' => ['required', 'confirmed', Password::defaults()],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
auth()->login($user);
|
||||
|
||||
return redirect('/videos');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth')->except(['channel']);
|
||||
}
|
||||
|
||||
// Profile page - view own profile
|
||||
public function profile()
|
||||
{
|
||||
$user = Auth::user();
|
||||
return view('user.profile', compact('user'));
|
||||
}
|
||||
|
||||
// Update profile
|
||||
public function updateProfile(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'avatar' => 'nullable|image|mimes:jpg,jpeg,png,webp|max:5120',
|
||||
]);
|
||||
|
||||
$data = ['name' => $request->name];
|
||||
|
||||
if ($request->hasFile('avatar')) {
|
||||
// Delete old avatar
|
||||
if ($user->avatar) {
|
||||
Storage::delete('public/avatars/' . $user->avatar);
|
||||
}
|
||||
$filename = Str::uuid() . '.' . $request->file('avatar')->getClientOriginalExtension();
|
||||
$request->file('avatar')->storeAs('public/avatars', $filename);
|
||||
$data['avatar'] = $filename;
|
||||
}
|
||||
|
||||
$user->update($data);
|
||||
|
||||
return redirect()->route('profile')->with('success', 'Profile updated successfully!');
|
||||
}
|
||||
|
||||
// Settings page
|
||||
public function settings()
|
||||
{
|
||||
$user = Auth::user();
|
||||
return view('user.settings', compact('user'));
|
||||
}
|
||||
|
||||
// Update settings (password)
|
||||
public function updateSettings(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$request->validate([
|
||||
'current_password' => 'required',
|
||||
'new_password' => 'required|min:8|confirmed',
|
||||
]);
|
||||
|
||||
if (!Hash::check($request->current_password, $user->password)) {
|
||||
return back()->withErrors(['current_password' => 'Current password is incorrect']);
|
||||
}
|
||||
|
||||
$user->update([
|
||||
'password' => Hash::make($request->new_password)
|
||||
]);
|
||||
|
||||
return redirect()->route('settings')->with('success', 'Password updated successfully!');
|
||||
}
|
||||
|
||||
// User's channel page - view videos
|
||||
public function channel($userId = null)
|
||||
{
|
||||
if ($userId) {
|
||||
$user = User::findOrFail($userId);
|
||||
} else {
|
||||
$user = Auth::user();
|
||||
}
|
||||
|
||||
// If viewing own channel, show all videos including private
|
||||
// If viewing someone else's channel, show only public videos
|
||||
if (Auth::check() && Auth::user()->id === $user->id) {
|
||||
$videos = Video::where('user_id', $user->id)
|
||||
->latest()
|
||||
->paginate(12);
|
||||
} else {
|
||||
$videos = Video::public()
|
||||
->where('user_id', $user->id)
|
||||
->latest()
|
||||
->paginate(12);
|
||||
}
|
||||
|
||||
return view('user.channel', compact('user', 'videos'));
|
||||
}
|
||||
|
||||
// Watch history
|
||||
public function history()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// Get videos the user has watched, ordered by most recently watched
|
||||
// Include private videos since they are the user's own
|
||||
$videoIds = \DB::table('video_views')
|
||||
->where('user_id', $user->id)
|
||||
->orderBy('watched_at', 'desc')
|
||||
->pluck('video_id')
|
||||
->unique();
|
||||
|
||||
$videos = Video::whereIn('id', $videoIds)
|
||||
->where(function($q) use ($user) {
|
||||
$q->where('visibility', '!=', 'private')
|
||||
->orWhere('user_id', $user->id);
|
||||
})
|
||||
->get()
|
||||
->sortByDesc(function ($video) use ($videoIds) {
|
||||
return $videoIds->search($video->id);
|
||||
});
|
||||
|
||||
return view('user.history', compact('videos'));
|
||||
}
|
||||
|
||||
// Liked videos
|
||||
public function liked()
|
||||
{
|
||||
$user = Auth::user();
|
||||
// Include private videos in liked (user's own private videos)
|
||||
$videos = $user->likes()
|
||||
->where(function($q) use ($user) {
|
||||
$q->where('visibility', '!=', 'private')
|
||||
->orWhere('videos.user_id', $user->id);
|
||||
})
|
||||
->latest()
|
||||
->paginate(12);
|
||||
|
||||
return view('user.liked', compact('videos'));
|
||||
}
|
||||
|
||||
// Like a video
|
||||
public function like(Video $video)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$video->isLikedBy($user)) {
|
||||
$video->likes()->attach($user->id);
|
||||
}
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
// Unlike a video
|
||||
public function unlike(Video $video)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$video->likes()->detach($user->id);
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
// Toggle like (API)
|
||||
public function toggleLike(Video $video)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($video->isLikedBy($user)) {
|
||||
$video->likes()->detach($user->id);
|
||||
$liked = false;
|
||||
} else {
|
||||
$video->likes()->attach($user->id);
|
||||
$liked = true;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'liked' => $liked,
|
||||
'like_count' => $video->like_count
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,49 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Jobs\CompressVideoJob;
|
||||
use App\Mail\VideoUploaded;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Support\Str;
|
||||
use FFMpeg\FFMpeg;
|
||||
use FFMpeg\FFProbe;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class VideoController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth')->except(['index', 'show', 'search', 'stream']);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$videos = Video::latest()->paginate(12);
|
||||
$videos = Video::public()->latest()->paginate(12);
|
||||
return view('videos.index', compact('videos'));
|
||||
}
|
||||
|
||||
public function search(Request $request)
|
||||
{
|
||||
$query = $request->get('q', '');
|
||||
|
||||
if (empty($query)) {
|
||||
return redirect()->route('videos.index');
|
||||
}
|
||||
|
||||
$videos = Video::public()
|
||||
->where(function($q) use ($query) {
|
||||
$q->where('title', 'like', "%{$query}%")
|
||||
->orWhere('description', 'like', "%{$query}%");
|
||||
})
|
||||
->latest()
|
||||
->paginate(12);
|
||||
|
||||
return view('videos.index', compact('videos', 'query'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('videos.create');
|
||||
@@ -25,88 +55,147 @@ class VideoController extends Controller
|
||||
$request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'video' => 'required|mimes:mp4,mov,avi,webm|max:2000000',
|
||||
'video' => 'required|file|mimes:mp4,webm,ogg,mov,avi,wmv,flv,mkv|max:2000000',
|
||||
'thumbnail' => 'nullable|image|mimes:jpg,jpeg,png,webp|max:5120',
|
||||
'orientation' => 'nullable|in:portrait,landscape,square,ultrawide',
|
||||
'visibility' => 'nullable|in:public,unlisted,private',
|
||||
]);
|
||||
|
||||
$file = $request->file('video');
|
||||
$filename = Str::uuid() . '.' . $file->getClientOriginalExtension();
|
||||
$path = $file->storeAs('videos', $filename, 'public');
|
||||
$videoFile = $request->file('video');
|
||||
$filename = Str::slug($request->title) . '-' . time() . '.' . $videoFile->getClientOriginalExtension();
|
||||
$path = $videoFile->storeAs('public/videos', $filename);
|
||||
|
||||
$width = 0;
|
||||
$height = 0;
|
||||
$duration = 0;
|
||||
$orientation = $request->orientation ?? 'landscape';
|
||||
|
||||
$videoPath = storage_path('app/public/videos/' . $filename);
|
||||
|
||||
if (!$request->orientation && file_exists($videoPath)) {
|
||||
$durationCmd = "ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 " . escapeshellarg($videoPath);
|
||||
$duration = (int) shell_exec($durationCmd);
|
||||
|
||||
$dimCmd = "ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 " . escapeshellarg($videoPath);
|
||||
$output = shell_exec($dimCmd);
|
||||
|
||||
if ($output) {
|
||||
$parts = explode('x', trim($output));
|
||||
if (count($parts) == 2) {
|
||||
$width = (int) trim($parts[0]);
|
||||
$height = (int) trim($parts[1]);
|
||||
// Get file info
|
||||
$fileSize = $videoFile->getSize();
|
||||
$mimeType = $videoFile->getMimeType();
|
||||
|
||||
$thumbnailPath = null;
|
||||
if ($request->hasFile('thumbnail')) {
|
||||
$thumbFilename = Str::uuid() . '.' . $request->file('thumbnail')->getClientOriginalExtension();
|
||||
$thumbnailPath = $request->file('thumbnail')->storeAs('public/thumbnails', $thumbFilename);
|
||||
} else {
|
||||
// Extract thumbnail from video using FFmpeg
|
||||
try {
|
||||
$ffmpeg = FFMpeg::create();
|
||||
$videoPath = storage_path('app/' . $path);
|
||||
|
||||
if (file_exists($videoPath)) {
|
||||
$video = $ffmpeg->open($videoPath);
|
||||
$frame = $video->frame(\FFMpeg\Coordinate\TimeCode::fromSeconds(1));
|
||||
|
||||
if ($height > $width * 1.2) {
|
||||
$orientation = 'portrait';
|
||||
} elseif ($width > $height * 1.5) {
|
||||
$orientation = 'ultrawide';
|
||||
} elseif (abs($width - $height) < 50) {
|
||||
$orientation = 'square';
|
||||
} else {
|
||||
$orientation = 'landscape';
|
||||
$thumbFilename = Str::uuid() . '.jpg';
|
||||
$thumbFullPath = storage_path('app/public/thumbnails/' . $thumbFilename);
|
||||
|
||||
// Ensure thumbnails directory exists
|
||||
if (!file_exists(storage_path('app/public/thumbnails'))) {
|
||||
mkdir(storage_path('app/public/thumbnails'), 0755, true);
|
||||
}
|
||||
|
||||
$frame->save($thumbFullPath);
|
||||
$thumbnailPath = 'public/thumbnails/' . $thumbFilename;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Log the error but don't fail the upload
|
||||
\Log::error('FFmpeg failed to extract thumbnail: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Get video dimensions and detect orientation using FFmpeg
|
||||
$width = null;
|
||||
$height = null;
|
||||
$orientation = 'landscape';
|
||||
|
||||
try {
|
||||
$ffprobe = FFProbe::create();
|
||||
$videoPath = storage_path('app/' . $path);
|
||||
|
||||
if (file_exists($videoPath)) {
|
||||
$streams = $ffprobe->streams($videoPath);
|
||||
$videoStream = $streams->videos()->first();
|
||||
|
||||
if ($videoStream) {
|
||||
$width = $videoStream->get('width');
|
||||
$height = $videoStream->get('height');
|
||||
|
||||
// Auto-detect orientation based on dimensions
|
||||
if ($width && $height) {
|
||||
if ($height > $width) {
|
||||
$orientation = 'portrait';
|
||||
} elseif ($width > $height) {
|
||||
$orientation = 'landscape';
|
||||
} else {
|
||||
$orientation = 'square';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Log the error but don't fail the upload
|
||||
\Log::error('FFprobe failed to get video dimensions: ' . $e->getMessage());
|
||||
// Use default orientation
|
||||
}
|
||||
|
||||
$thumbnailFilename = null;
|
||||
|
||||
if ($request->hasFile('thumbnail')) {
|
||||
$thumbFile = $request->file('thumbnail');
|
||||
$thumbnailFilename = Str::uuid() . '.' . $thumbFile->getClientOriginalExtension();
|
||||
$thumbFile->storeAs('thumbnails', $thumbnailFilename, 'public');
|
||||
} elseif (file_exists($videoPath)) {
|
||||
$thumbFilename = Str::uuid() . '.jpg';
|
||||
$thumbPath = storage_path('app/public/thumbnails/' . $thumbFilename);
|
||||
$cmd = "ffmpeg -i " . escapeshellarg($videoPath) . " -ss 00:00:01 -vframes 1 -vf 'scale=320:-1' " . escapeshellarg($thumbPath) . " -y 2>/dev/null";
|
||||
shell_exec($cmd);
|
||||
if (file_exists($thumbPath)) {
|
||||
$thumbnailFilename = $thumbFilename;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$video = Video::create([
|
||||
'user_id' => Auth::id(),
|
||||
'title' => $request->title,
|
||||
'description' => $request->description,
|
||||
'filename' => $filename,
|
||||
'path' => 'videos',
|
||||
'thumbnail' => $thumbnailFilename,
|
||||
'duration' => $duration,
|
||||
'path' => $path,
|
||||
'thumbnail' => $thumbnailPath ? basename($thumbnailPath) : null,
|
||||
'size' => $fileSize,
|
||||
'mime_type' => $mimeType,
|
||||
'orientation' => $orientation,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'orientation' => $orientation,
|
||||
'size' => $file->getSize(),
|
||||
'status' => 'ready',
|
||||
'status' => 'processing',
|
||||
'visibility' => $request->visibility ?? 'public',
|
||||
]);
|
||||
|
||||
// Dispatch compression job in the background
|
||||
CompressVideoJob::dispatch($video);
|
||||
|
||||
// Load user relationship for email
|
||||
$video->load('user');
|
||||
|
||||
// Send email notification
|
||||
try {
|
||||
Mail::to(Auth::user()->email)->send(new VideoUploaded($video));
|
||||
} catch (\Exception $e) {
|
||||
// Log the error but don't fail the upload
|
||||
\Log::error('Email notification failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Video uploaded successfully!',
|
||||
'video_id' => $video->id,
|
||||
'redirect' => route('videos.show', $video->id)
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Video $video)
|
||||
{
|
||||
// Check if user can view this video
|
||||
if (!$video->canView(Auth::user())) {
|
||||
abort(404, 'Video not found');
|
||||
}
|
||||
|
||||
// Track view if user is logged in
|
||||
if (Auth::check()) {
|
||||
$user = Auth::user();
|
||||
// Add view if not already viewed recently (within last hour)
|
||||
$existingView = \DB::table('video_views')
|
||||
->where('user_id', $user->id)
|
||||
->where('video_id', $video->id)
|
||||
->where('watched_at', '>', now()->subHour())
|
||||
->first();
|
||||
|
||||
if (!$existingView) {
|
||||
\DB::table('video_views')->insert([
|
||||
'user_id' => $user->id,
|
||||
'video_id' => $video->id,
|
||||
'watched_at' => now()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return view('videos.show', compact('video'));
|
||||
}
|
||||
|
||||
@@ -121,93 +210,101 @@ class VideoController extends Controller
|
||||
'title' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'thumbnail' => 'nullable|image|mimes:jpg,jpeg,png,webp|max:5120',
|
||||
'orientation' => 'nullable|in:portrait,landscape,square,ultrawide',
|
||||
'visibility' => 'nullable|in:public,unlisted,private',
|
||||
]);
|
||||
|
||||
$video->title = $request->title;
|
||||
$video->description = $request->description;
|
||||
|
||||
if ($request->orientation) {
|
||||
$video->orientation = $request->orientation;
|
||||
}
|
||||
$data = $request->only(['title', 'description', 'visibility']);
|
||||
|
||||
if ($request->hasFile('thumbnail')) {
|
||||
if ($video->thumbnail) {
|
||||
$oldThumb = storage_path('app/public/thumbnails/' . $video->thumbnail);
|
||||
if (file_exists($oldThumb)) {
|
||||
unlink($oldThumb);
|
||||
}
|
||||
Storage::delete('public/thumbnails/' . $video->thumbnail);
|
||||
}
|
||||
|
||||
$thumbFile = $request->file('thumbnail');
|
||||
$thumbnailFilename = Str::uuid() . '.' . $thumbFile->getClientOriginalExtension();
|
||||
$thumbFile->storeAs('thumbnails', $thumbnailFilename, 'public');
|
||||
$video->thumbnail = $thumbnailFilename;
|
||||
$thumbFilename = Str::uuid() . '.' . $request->file('thumbnail')->getClientOriginalExtension();
|
||||
$data['thumbnail'] = $request->file('thumbnail')->storeAs('public/thumbnails', $thumbFilename);
|
||||
$data['thumbnail'] = basename($data['thumbnail']);
|
||||
}
|
||||
|
||||
$video->save();
|
||||
|
||||
return redirect()->route('videos.show', $video->id)
|
||||
->with('success', 'Video updated successfully!');
|
||||
}
|
||||
|
||||
public function stream(Video $video)
|
||||
{
|
||||
$filePath = storage_path('app/public/videos/' . $video->filename);
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
abort(404, 'Video file not found');
|
||||
// Set default visibility if not provided
|
||||
if (!isset($data['visibility'])) {
|
||||
unset($data['visibility']);
|
||||
}
|
||||
|
||||
$fileSize = filesize($filePath);
|
||||
$range = request()->header('Range');
|
||||
$video->update($data);
|
||||
|
||||
header('Content-Type: video/mp4');
|
||||
header('Accept-Ranges: bytes');
|
||||
|
||||
if ($range) {
|
||||
$ranges = explode('-', str_replace('bytes=', '', $range));
|
||||
$start = intval($ranges[0]);
|
||||
$end = isset($ranges[1]) && $ranges[1] ? intval($ranges[1]) : $fileSize - 1;
|
||||
$length = $end - $start + 1;
|
||||
|
||||
header('HTTP/1.1 206 Partial Content');
|
||||
header('Content-Length: ' . $length);
|
||||
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
|
||||
|
||||
$fp = fopen($filePath, 'rb');
|
||||
fseek($fp, $start);
|
||||
$chunkSize = 8192;
|
||||
while (!feof($fp) && (ftell($fp) <= $end)) {
|
||||
$chunk = fread($fp, min($chunkSize, $end - ftell($fp) + 1));
|
||||
echo $chunk;
|
||||
flush();
|
||||
}
|
||||
fclose($fp);
|
||||
} else {
|
||||
header('Content-Length: ' . $fileSize);
|
||||
readfile($filePath);
|
||||
}
|
||||
exit;
|
||||
return redirect()->route('videos.show', $video)->with('success', 'Video updated!');
|
||||
}
|
||||
|
||||
public function destroy(Video $video)
|
||||
{
|
||||
$path = storage_path('app/public/videos/' . $video->filename);
|
||||
if (file_exists($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
|
||||
Storage::delete('public/videos/' . $video->filename);
|
||||
if ($video->thumbnail) {
|
||||
$thumbPath = storage_path('app/public/thumbnails/' . $video->thumbnail);
|
||||
if (file_exists($thumbPath)) {
|
||||
unlink($thumbPath);
|
||||
}
|
||||
Storage::delete('public/thumbnails/' . $video->thumbnail);
|
||||
}
|
||||
$video->delete();
|
||||
return redirect()->route('videos.index')->with('success', 'Video deleted!');
|
||||
}
|
||||
|
||||
public function stream(Video $video)
|
||||
{
|
||||
// Check if user can view this video
|
||||
if (!$video->canView(Auth::user())) {
|
||||
abort(404, 'Video not found');
|
||||
}
|
||||
|
||||
$path = storage_path('app/public/videos/' . $video->filename);
|
||||
|
||||
if (!file_exists($path)) {
|
||||
abort(404, 'Video file not found');
|
||||
}
|
||||
|
||||
$video->delete();
|
||||
$fileSize = filesize($path);
|
||||
$mimeType = $video->mime_type ?: 'video/mp4';
|
||||
|
||||
return redirect()->route('videos.index')
|
||||
->with('success', 'Video deleted successfully!');
|
||||
$handle = fopen($path, 'rb');
|
||||
if (!$handle) {
|
||||
abort(500, 'Cannot open video file');
|
||||
}
|
||||
|
||||
$range = request()->header('Range');
|
||||
|
||||
if ($range) {
|
||||
// Parse range header
|
||||
preg_match('/bytes=(\d+)-(\d*)/', $range, $matches);
|
||||
$start = intval($matches[1] ?? 0);
|
||||
$end = $matches[2] ? intval($matches[2]) : $fileSize - 1;
|
||||
|
||||
$length = $end - $start + 1;
|
||||
|
||||
header('HTTP/1.1 206 Partial Content');
|
||||
header('Content-Type: ' . $mimeType);
|
||||
header('Content-Length: ' . $length);
|
||||
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
|
||||
header('Accept-Ranges: bytes');
|
||||
header('Cache-Control: public, max-age=3600');
|
||||
|
||||
fseek($handle, $start);
|
||||
$chunkSize = 8192;
|
||||
$bytesToRead = $length;
|
||||
|
||||
while (!feof($handle) && $bytesToRead > 0) {
|
||||
$buffer = fread($handle, min($chunkSize, $bytesToRead));
|
||||
echo $buffer;
|
||||
flush();
|
||||
$bytesToRead -= strlen($buffer);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
exit;
|
||||
} else {
|
||||
// No range requested, stream entire file
|
||||
header('Content-Type: ' . $mimeType);
|
||||
header('Content-Length: ' . $fileSize);
|
||||
header('Accept-Ranges: bytes');
|
||||
header('Cache-Control: public, max-age=3600');
|
||||
|
||||
fpassthru($handle);
|
||||
fclose($handle);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user