Skip to content

Instantly share code, notes, and snippets.

@sujit-baniya
Created September 6, 2022 04:38
Show Gist options
  • Save sujit-baniya/733c7335c39eb6e7ff08f1eba9e0bf77 to your computer and use it in GitHub Desktop.
Save sujit-baniya/733c7335c39eb6e7ff08f1eba9e0bf77 to your computer and use it in GitHub Desktop.
<script setup>
import { Head, Link } from '@inertiajs/inertia-vue3';
import '@vime/core/themes/default.css';
import {computed, ref} from "vue";
import {getLangByCode} from '~/Data/lang'
import {
ArrowLeftIcon
} from '@heroicons/vue/solid'
const props = defineProps(['canDownload', 'adsense', 'video', 'poster', 'subtitles', 'id', 'settings', 'source', 'canDownload', 'videos'])
const videoSettings = ref(props.settings?.settings ? JSON.parse(props.settings.settings) : null)
const videoLink = ref(null)
const getVideoLink = (video) => {
if (!videoLink.value) {
videoLink.value = video
}
if (video) {
return route('stream.video', {id: props.id, filename: video})
}
return null
}
const getPosterLink = computed(() => {
if (props.poster) {
return route('view.file.content', {id: props.id, filename: props.poster})
}
return null
})
const getSubtitleLink = (subtitle) => {
return route('view.file.content', {id: props.id, filename: subtitle})
}
const downloadVideo = (t) => {
let link = route('download.video', {id: props.id, filename: videoLink.value})
window.open(link, '_blank');
}
const options = ref({
quality: {
default: '1080p'
},
controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume','settings','pip','download','fullscreen'],
// quality: { default: 576, options: [1080, 720, 576] },
storage: { enabled: false },
ads: {
enabled: !!props.adsense,
tagUrl: props.adsense,
}
})
const isVideoOnly = computed(() => {
return !!(videoSettings.value && videoSettings.value.videos && !props.video);
})
const isYoutubeVideo = computed(() => {
return !!(!(videoSettings.value && videoSettings.value.videos && !props.video) && videoSettings.value.external.youtube);
})
const isVimeoVideo = computed(() => {
return !!(!(videoSettings.value && videoSettings.value.videos && !props.video) && videoSettings.value.external.vimeo);
})
const isTrailer = computed(() => {
return !!props.video;
})
</script>
<template>
<vue-plyr v-if="isVideoOnly" :options="options">
<video controls
crossorigin
playsinline
:data-poster="getPosterLink">
<template v-for="vid in videoSettings.videos">
<source :src="getVideoLink(vid)"/>
</template>
<track v-for="(subtitle, code) in videoSettings?.subtitles"
default
kind="subtitles"
:src="getSubtitleLink(subtitle)"
:srclang="code"
:label="getLangByCode(code).label"
/>
</video>
</vue-plyr>
<!-- youtube div element -->
<vue-plyr
v-if="isYoutubeVideo" :options="options">
<div data-plyr-provider="youtube" :data-plyr-embed-id="videoSettings?.external?.youtube"></div>
</vue-plyr>
<!-- youtube div element -->
<vue-plyr
v-if="isTrailer" :options="options">
<div data-plyr-provider="youtube" :data-plyr-embed-id="video"></div>
</vue-plyr>
<!-- vimeo div element -->
<vue-plyr
v-if="isVimeoVideo" :options="options">
<div data-plyr-provider="vimeo" :data-plyr-embed-id="videoSettings?.external?.vimeo"></div>
</vue-plyr>
<Link :href="route('browse')" class="flex text-white mt-2 hover:text-gray-200" as="button">
<ArrowLeftIcon class="h-5 w-5 mr-1"/>
<span>Back</span>
</Link>
</template>
<?php
namespace App\Services;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use League\Flysystem\Filesystem;
use Symfony\Component\HttpFoundation\StreamedResponse;
class VideoStream {
/**
* Name of adapter
*
* @var string
*/
private $adapterName;
/**
* Storage disk
*
* @var FilesystemAdapter
*/
private $disk;
/**
* @var int file end byte
*/
private $end;
/**
* @var string
*/
private $filePath;
/**
* Human-known filename
*
* @var string|null
*/
private $humanName;
/**
* @var bool storing if request is a range (or a full file)
*/
private $isRange = false;
/**
* @var int|null length of bytes requested
*/
private $length = null;
/**
* @var array
*/
private $returnHeaders = [];
/**
* @var int file size
*/
private $size;
/**
* @var int start byte
*/
private $start;
/**
* S3FileStream constructor.
* @param string $filePath
* @param string $adapter
* @param string $humanName
*/
public function __construct(string $filePath, string $adapter = 's3', ?string $humanName = null)
{
$options = [
'region' => env("AWS_DEFAULT_REGION"),
'version' => 'latest'
];
$this->filePath = $filePath;
$this->adapterName = $adapter;
$this->disk = Storage::disk($this->adapterName);
$this->client = new \Aws\S3\S3Client($options);
$this->humanName = $humanName;
$this->start = 0;
$this->size = 0;
$this->end = 0;
}
/**
* Output file to client.
*/
public function output()
{
return $this->setHeadersAndStream();
}
/**
* Output headers to client.
* @return Response|StreamedResponse
*/
protected function setHeadersAndStream()
{
if (!$this->disk->exists($this->filePath)) {
report(new Exception('S3 File Not Found in S3FileStream - ' . $this->adapterName . ' - ' . $this->disk->path($this->filePath)));
throw new FileNotFoundException("File not found " . $this->filePath, 404);
}
$this->start = 0;
$this->size = $this->disk->size($this->filePath);
$this->end = $this->size - 1;
$this->length = $this->size;
$this->isRange = false;
//Set headers
$this->returnHeaders = [
'Last-Modified' => $this->disk->lastModified($this->filePath),
'Accept-Ranges' => 'bytes',
'Content-Type' => $this->disk->mimeType($this->filePath),
'Content-Disposition' => 'inline; filename=' . ($this->humanName ?? basename($this->filePath) . '.' . Arr::last(explode('.', $this->filePath))),
'Content-Length' => $this->length,
];
//Handle ranges here
if (!is_null(request()->server('HTTP_RANGE'))) {
$cStart = $this->start;
$cEnd = $this->end;
$range = Str::after(request()->server('HTTP_RANGE'), '=');
if (strpos($range, ',') !== false) {
return response('416 Requested Range Not Satisfiable', 416, [
'Content-Range' => 'bytes */' . $this->size,
]);
}
if (substr($range, 0, 1) == '-') {
$cStart = $this->size - intval(substr($range, 1)) - 1;
} else {
$range = explode('-', $range);
$cStart = intval($range[0]);
$cEnd = (isset($range[1]) && is_numeric($range[1])) ? intval($range[1]) : $cEnd;
}
$cEnd = min($cEnd, $this->size - 1);
if ($cStart > $cEnd || $cStart > $this->size - 1) {
return response('416 Requested Range Not Satisfiable', 416, [
'Content-Range' => 'bytes */' . $this->size,
]);
}
$this->start = intval($cStart);
$this->end = intval($cEnd);
$this->length = min($this->end - $this->start + 1, $this->size);
$this->returnHeaders['Content-Length'] = $this->length;
$this->returnHeaders['Content-Range'] = 'bytes ' . $this->start . '-' . $this->end . '/' . $this->size;
$this->isRange = true;
}
return $this->stream();
}
/**
* Stream file to client.
* @throws Exception
* @return StreamedResponse
*/
protected function stream(): StreamedResponse
{
$stream = $this->disk->readStream($this->filePath);
if (isset($this->start) && $this->start > 0) {
fseek($stream, $this->start, SEEK_SET);
}
$remainingBytes = $this->length ?? $this->size;
$chunkSize = 100;
return response()->stream(
function () use ($stream, $remainingBytes, $chunkSize) {
while (!feof($stream) && $remainingBytes > 0) {
$toGrab = min($chunkSize, $remainingBytes);
echo fread($stream, $toGrab);
$remainingBytes -= $toGrab;
flush();
}
fclose($stream);
},
($this->isRange ? 206 : 200),
$this->returnHeaders
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment