Skip to content

Commit 0420c50

Browse files
authored
Merge pull request #1 from pascalbaljetmedia/hls-stream
HLS support
2 parents a002fc3 + 3589094 commit 0420c50

File tree

8 files changed

+473
-1
lines changed

8 files changed

+473
-1
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,24 @@ $frame = $media->getFrameFromTimecode($timecode);
140140

141141
```
142142

143+
Create a M3U8 playlist to do [HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming):
144+
145+
``` php
146+
$lowBitrate = (new X264)->setKiloBitrate(250);
147+
$midBitrate = (new X264)->setKiloBitrate(500);
148+
$highBitrate = (new X264)->setKiloBitrate(1000);
149+
150+
FFMpeg::fromDisk('videos')
151+
->open('steve_howe.mp4')
152+
->exportPlaylistForHLS()
153+
->setSegmentLength(10) // optional
154+
->addFormat($lowBitrate)
155+
->addFormat($midBitrate)
156+
->addFormat($highBitrate)
157+
->save('adaptive_steve.m3u8');
158+
159+
```
160+
143161
## Changelog
144162

145163
Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.

src/HLSPlaylistExporter.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
namespace Pbmedia\LaravelFFMpeg;
4+
5+
use FFMpeg\Format\VideoInterface;
6+
use Pbmedia\LaravelFFMpeg\SegmentedExporter;
7+
8+
class HLSPlaylistExporter extends MediaExporter
9+
{
10+
protected $formats = [];
11+
12+
protected $segmentLength = 10;
13+
14+
protected $segmentedExporters;
15+
16+
protected $saveMethod = 'savePlaylist';
17+
18+
public function addFormat(VideoInterface $format): MediaExporter
19+
{
20+
$this->formats[] = $format;
21+
22+
return $this;
23+
}
24+
25+
public function getFormatsSorted(): array
26+
{
27+
usort($this->formats, function ($formatA, $formatB) {
28+
return $formatA->getKiloBitrate() <=> $formatB->getKiloBitrate();
29+
});
30+
31+
return $this->formats;
32+
}
33+
34+
public function setPlaylistPath(string $playlistPath): MediaExporter
35+
{
36+
$this->playlistPath = $playlistPath;
37+
38+
return $this;
39+
}
40+
41+
public function setSegmentLength(int $segmentLength): MediaExporter
42+
{
43+
$this->segmentLength = $segmentLength;
44+
45+
return $this;
46+
}
47+
48+
protected function getSegmentedExporterFromFormat(VideoInterface $format): SegmentedExporter
49+
{
50+
$media = clone $this->media;
51+
52+
return (new SegmentedExporter($media))
53+
->inFormat($format)
54+
->setPlaylistPath($this->playlistPath)
55+
->setSegmentLength($this->segmentLength);
56+
}
57+
58+
public function getSegmentedExporters(): array
59+
{
60+
if ($this->segmentedExporters) {
61+
return $this->segmentedExporters;
62+
}
63+
64+
return $this->segmentedExporters = array_map(function ($format) {
65+
return $this->getSegmentedExporterFromFormat($format);
66+
}, $this->getFormatsSorted());
67+
}
68+
69+
protected function exportStreams()
70+
{
71+
foreach ($this->getSegmentedExporters() as $segmentedExporter) {
72+
$segmentedExporter->saveStream($this->playlistPath);
73+
}
74+
}
75+
76+
protected function getMasterPlaylistContents(): string
77+
{
78+
$lines = ['#EXTM3U'];
79+
80+
foreach ($this->getSegmentedExporters() as $segmentedExporter) {
81+
$bitrate = $segmentedExporter->getFormat()->getKiloBitrate() * 1000;
82+
83+
$lines[] = '#EXT-X-STREAM-INF:BANDWIDTH=' . $bitrate;
84+
$lines[] = $segmentedExporter->getPlaylistFilename();
85+
}
86+
87+
return implode(PHP_EOL, $lines);
88+
}
89+
90+
public function savePlaylist(string $playlistPath): MediaExporter
91+
{
92+
$this->setPlaylistPath($playlistPath);
93+
$this->exportStreams();
94+
95+
file_put_contents(
96+
$playlistPath,
97+
$this->getMasterPlaylistContents()
98+
);
99+
100+
return $this;
101+
}
102+
}

src/Media.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ public function export(): MediaExporter
3636
return new MediaExporter($this);
3737
}
3838

39+
public function exportForHLS(): HLSPlaylistExporter
40+
{
41+
return new HLSPlaylistExporter($this);
42+
}
43+
3944
public function getFrameFromString(string $timecode): Frame
4045
{
4146
return $this->getFrameFromTimecode(
@@ -75,6 +80,22 @@ protected function selfOrArgument($argument)
7580
return ($argument === $this->media) ? $this : $argument;
7681
}
7782

83+
public function __invoke(): MediaTypeInterface
84+
{
85+
return $this->media;
86+
}
87+
88+
public function __clone()
89+
{
90+
if ($this->media) {
91+
$clonedFilters = clone $this->media->getFiltersCollection();
92+
93+
$this->media = clone $this->media;
94+
95+
$this->media->setFiltersCollection($clonedFilters);
96+
}
97+
}
98+
7899
public function __call($method, $parameters)
79100
{
80101
return $this->selfOrArgument(

src/MediaExporter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public function __construct(Media $media)
2121
$this->disk = $media->getFile()->getDisk();
2222
}
2323

24-
protected function getFormat(): FormatInterface
24+
public function getFormat(): FormatInterface
2525
{
2626
return $this->format;
2727
}

src/SegmentedExporter.php

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
namespace Pbmedia\LaravelFFMpeg;
4+
5+
class SegmentedExporter extends MediaExporter
6+
{
7+
protected $filter;
8+
9+
protected $playlistPath;
10+
11+
protected $segmentLength = 10;
12+
13+
protected $saveMethod = 'saveStream';
14+
15+
public function setPlaylistPath(string $playlistPath): MediaExporter
16+
{
17+
$this->playlistPath = $playlistPath;
18+
19+
return $this;
20+
}
21+
22+
public function setSegmentLength(int $segmentLength): MediaExporter
23+
{
24+
$this->segmentLength = $segmentLength;
25+
26+
return $this;
27+
}
28+
29+
public function getFilter(): SegmentedFilter
30+
{
31+
if ($this->filter) {
32+
return $this->filter;
33+
}
34+
35+
return $this->filter = new SegmentedFilter(
36+
$this->getPlaylistFilename(),
37+
$this->segmentLength
38+
);
39+
}
40+
41+
public function saveStream(string $playlistPath): MediaExporter
42+
{
43+
$this->setPlaylistPath($playlistPath);
44+
45+
$this->media->addFilter(
46+
$this->getFilter()
47+
);
48+
49+
$this->media->save(
50+
$this->getFormat(),
51+
$this->getSegmentFullPath()
52+
);
53+
54+
return $this;
55+
}
56+
57+
public function getSegmentFullPath(): string
58+
{
59+
return implode(DIRECTORY_SEPARATOR, [
60+
pathinfo($this->playlistPath, PATHINFO_DIRNAME),
61+
$this->getSegmentFilename(),
62+
]);
63+
}
64+
65+
public function getPlaylistPath(): string
66+
{
67+
return $this->playlistPath;
68+
}
69+
70+
public function getPlaylistName(): string
71+
{
72+
return pathinfo($this->getPlaylistPath(), PATHINFO_FILENAME);
73+
}
74+
75+
public function getPlaylistFilename(): string
76+
{
77+
return $this->getFormattedFilename('.m3u8');
78+
}
79+
80+
public function getSegmentFilename(): string
81+
{
82+
return $this->getFormattedFilename('_%05d.ts');
83+
}
84+
85+
protected function getFormattedFilename(string $suffix = ''): string
86+
{
87+
return implode([
88+
$this->getPlaylistName(),
89+
'_',
90+
$this->getFormat()->getKiloBitrate(),
91+
]) . $suffix;
92+
}
93+
}

src/SegmentedFilter.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace Pbmedia\LaravelFFMpeg;
4+
5+
use FFMpeg\Filters\Video\VideoFilterInterface;
6+
use FFMpeg\Format\VideoInterface;
7+
use FFMpeg\Media\Video;
8+
9+
class SegmentedFilter implements VideoFilterInterface
10+
{
11+
protected $playlistPath;
12+
13+
protected $segmentLength;
14+
15+
protected $priority;
16+
17+
public function __construct(string $playlistPath, int $segmentLength = 10, $priority = 0)
18+
{
19+
$this->playlistPath = $playlistPath;
20+
$this->segmentLength = $segmentLength;
21+
$this->priority = $priority;
22+
}
23+
24+
public function getPlaylistPath(): string
25+
{
26+
return $this->playlistPath;
27+
}
28+
29+
public function getSegmentLength(): int
30+
{
31+
return $this->segmentLength;
32+
}
33+
34+
public function getPriority()
35+
{
36+
return $this->priority;
37+
}
38+
39+
public function apply(Video $video, VideoInterface $format)
40+
{
41+
return [
42+
'-map',
43+
'0',
44+
'-flags',
45+
'-global_header',
46+
'-f',
47+
'segment',
48+
'-segment_format',
49+
'mpeg_ts',
50+
'-segment_list',
51+
$this->getPlaylistPath(),
52+
'-segment_time',
53+
$this->getSegmentLength(),
54+
];
55+
}
56+
}

0 commit comments

Comments
 (0)