VideoStreamer   A
last analyzed

Complexity

Total Complexity 19

Size/Duplication

Total Lines 139
Duplicated Lines 0 %

Importance

Changes 10
Bugs 5 Features 2
Metric Value
eloc 74
c 10
b 5
f 2
dl 0
loc 139
rs 10
wmc 19

6 Methods

Rating   Name   Duplication   Size   Complexity  
B setHeader() 0 51 10
A __construct() 0 8 1
A end() 0 5 1
A stream() 0 18 4
A start() 0 5 1
A streamFile() 0 8 2
1
<?php
2
3
namespace Iman\Streamer;
4
5
use Iman\Streamer\Events\VideoStreamEnded;
6
use Iman\Streamer\Events\VideoStreamStarted;
7
8
class VideoStreamer
9
{
10
    private $path = '';
11
12
    private $stream = '';
13
14
    private $buffer = 102400;
15
16
    private $start = -1;
17
18
    private $end = -1;
19
20
    private $size = 0;
21
22
    private $mime = '';
23
24
    /**
25
     * @var Video
26
     */
27
    private $video;
28
29
    public function __construct($filePath, $stream)
30
    {
31
        $this->path = $filePath;
32
        $this->stream = $stream;
33
        $this->mime = mime_content_type($filePath);
34
35
        $this->video = new Video();
36
        $this->video->setPath($this->path);
37
    }
38
39
    /**
40
     * Set proper header to serve the video content.
41
     */
42
    private function setHeader()
43
    {
44
        ob_get_clean();
45
        header('Content-Type: '.$this->mime);
46
        header('Cache-Control: max-age=2592000, public');
47
        header('Expires: '.gmdate('D, d M Y H:i:s', time() + 2592000).' GMT');
48
        header('Last-Modified: '.gmdate('D, d M Y H:i:s', @filemtime($this->path)).' GMT');
0 ignored issues
show
Bug introduced by
It seems like @filemtime($this->path) can also be of type false; however, parameter $timestamp of gmdate() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

48
        header('Last-Modified: '.gmdate('D, d M Y H:i:s', /** @scrutinizer ignore-type */ @filemtime($this->path)).' GMT');
Loading history...
49
        $this->start = 0;
50
        $this->size = filesize($this->path);
51
        $this->end = $this->size - 1;
52
        header('Accept-Ranges: 0-'.$this->end);
53
54
        if (! isset($_SERVER['HTTP_RANGE'])) {
55
            header('Content-Length: '.$this->size);
56
57
            return;
58
        }
59
60
        $c_end = $this->end;
61
        [
62
            ,
63
            $range,
64
        ] = explode('=', $_SERVER['HTTP_RANGE'], 2);
65
66
        if (strpos($range, ',') !== false) {
67
            header('HTTP/1.1 416 Requested Range Not Satisfiable');
68
            header("Content-Range: bytes $this->start-$this->end/$this->size");
69
            exit;
70
        }
71
72
        if ($range == '-') {
73
            $c_start = $this->size - substr($range, 1);
74
        } else {
75
            $range = explode('-', $range);
76
            $c_start = $range[0];
77
78
            $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end;
79
        }
80
        $c_end = ($c_end > $this->end) ? $this->end : $c_end;
81
        if ($c_start > $c_end || $c_start > $this->size - 1 || $c_end >= $this->size) {
82
            header('HTTP/1.1 416 Requested Range Not Satisfiable');
83
            header("Content-Range: bytes $this->start-$this->end/$this->size");
84
            exit;
85
        }
86
        $this->start = $c_start;
87
        $this->end = $c_end;
88
        $length = $this->end - $this->start + 1;
89
        fseek($this->stream, $this->start);
0 ignored issues
show
Bug introduced by
It seems like $this->start can also be of type string; however, parameter $offset of fseek() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

89
        fseek($this->stream, /** @scrutinizer ignore-type */ $this->start);
Loading history...
90
        header('HTTP/1.1 206 Partial Content');
91
        header('Content-Length: '.$length);
92
        header("Content-Range: bytes $this->start-$this->end/".$this->size);
93
    }
94
95
    /**
96
     * close curretly opened stream.
97
     */
98
    private function end()
99
    {
100
        fclose($this->stream);
101
        event(new VideoStreamEnded($this->video));
102
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
103
    }
104
105
    /**
106
     * perform the streaming of calculated range.
107
     */
108
    private function stream()
109
    {
110
        $this->video->setProgress($this->start);
111
        event(new VideoStreamStarted($this->video));
112
113
        $i = $this->start;
114
        set_time_limit(0);
115
        while (! feof($this->stream) && $i <= $this->end) {
116
            $this->video->setProgress($i);
117
118
            $bytesToRead = $this->buffer;
119
            if (($i + $bytesToRead) > $this->end) {
120
                $bytesToRead = $this->end - $i + 1;
121
            }
122
            $data = fread($this->stream, $bytesToRead);
123
            echo $data;
124
            flush();
125
            $i += $bytesToRead;
126
        }
127
    }
128
129
    /**
130
     * Start streaming video content.
131
     */
132
    public function start()
133
    {
134
        $this->setHeader();
135
        $this->stream();
136
        $this->end();
137
    }
138
139
    public static function streamFile($path)
140
    {
141
        $stream = fopen($path, 'rb');
142
        if (! $stream) {
0 ignored issues
show
introduced by
$stream is of type resource, thus it always evaluated to false.
Loading history...
143
            throw new \Exception('File not found in: '.$path, 6542);
144
        }
145
146
        (new static($path, $stream))->start();
147
    }
148
}
149