· 9 years ago · Dec 16, 2016, 06:14 AM
1///////////////////
2// QUESTION //
3//////////////////
4
5/*
6How to create dynamic m3u8 by pasting the URL in browser?
7
8I want to create a dynamic m3u8 when a PHP script is called. I don't want to save the result m3u8 on server, instead I want to push it to browser so it is downloadable. Could anyone show me how I can achieve this task?
9
10Example of PHP script to be called:
11
12http://www.asite.com/makeM3u8.php?videoId=1234
13
14Downloadable dynamic m3u8 structure:
15
16#EXTM3U
17#EXT-X-VERSION:3
18#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=900000
19http://someserver/channelNameBandwith900000.m3u8?session=3495732948523984eriuwehiurweirew
20php m3u8
21*/
22
23
24///////////////////
25// ANSWER //
26//////////////////
27/*
28You have to decide a number of things before getting to the script:
29
301.- Where are the .ts and .aac files stored, what is their protection scheme and is PHP able to create a URL that is able to access them?
31
322.- Where you store the raw m3u8 information (target-duration, extinf and name for each segment). Database is faster than parsing existing files in this case.
33
343.- If dealing with multibitrate, you need a script that also generates the master m3u8 which points to all the others.
35
36That being said, here is the solution I came up with and have been using for a while without problems. Two things though, I use AWS S3 for storage and convert existing video files with ffmpeg. It is a rather long process - maybe overkill for what you want - but it works.
37
38Part 1.- Encoding the files The system receives MP4 videos and converts them to the requisite formats.
39*/
40function ffConvert($origin,$basedir,$res) {
41switch($res) {
42 // SET THE VARIABLES
43 case "240p": $size = "426x240"; $vbit = "360k"; $level = "3.0"; $abit = "80k"; break;
44 case "480p": $size = "854x480"; $vbit = "784k"; $level = "3.1"; $abit = "128k"; break;
45 case "720p": $size = "1280x720"; $vbit = "1648k"; $level = "3.1"; $abit = "192k"; break;
46}
47// CONVERT THE FILES
48exec('/usr/local/bin/ffmpeg -y -async 1 -vsync -1 -analyzeduration 999999999 -i "'.$origin.'" -force_key_frames "expr:gte(t,n_forced*10)" -pix_fmt yuv420p -s '.$size.' -r:v 30 -b:v '.$vbit.' -c:v libx264 -profile:v baseline -level '.$level.' -c:a libfaac -ac 2 -ar 48000 -b:a '.$abit.' -g 90 '.$base.$res.'.mp4');
49// VERIFY AND RETURN
50if(file_exists($basedir.$res.'.mp4')) {
51 return TRUE;
52} else {
53 return FALSE;
54}
55}
56
57/*
58Part 2.- Segmenting the files The system takes the converted MP4s and segments them.
59*/
60
61function ffSegment($basedir,$res) {
62// CREATE THE SEGMENTS
63mkdir($basedir.$res, 0775);
64exec('/usr/local/bin/ffmpeg -y -analyzeduration 999999999 -i "'.$basedir.$res.'.mp4" -codec copy -map 0 -f segment -segment_list "'.$basedir.$res.'/stream.m3u8" -segment_time 10 -segment_list_type m3u8 -bsf:v h264_mp4toannexb "'.$basedir.$res.'/segment%05d.ts"');
65if(file_exists($basedir.$res.'/stream.m3u8')) {
66 return TRUE;
67} else {
68 return FALSE;
69}
70}
71
72/*
73Part 3.- Storing the data The system parses the generated m3u8s and stores the relevant information in a database.
74
75Table:
76*/
77
78
79CREATE TABLE IF NOT EXISTS `data_contenido_archivos` (
80 `id` bigint(20) unsigned NOT NULL,
81 `240p` mediumtext NOT NULL,
82 `480p` mediumtext NOT NULL,
83 `720p` mediumtext NOT NULL,
84 `aac` mediumtext NOT NULL,
85 UNIQUE KEY `id` (`id`)
86) ENGINE=TokuDB DEFAULT CHARSET=utf8;
87
88/*
89Parse function:
90*/
91
92function parseHLS($file) {
93$return = array();
94$i = 0;
95$handle = fopen($file, "r");
96if($handle) {
97 while(($line = fgets($handle)) !== FALSE) {
98 if(strpos($line,"#EXTINF") !== FALSE) {
99 $return['data'][$i]['inf'] = str_replace(array("#EXTINF:",",","\r","\n"),array("","","",""),$line);
100 }
101 if(strpos($line,".ts") !== FALSE) {
102 $return['data'][$i]['ts'] = str_replace(array(".ts","segment","\r","\n"),array("","","",""),$line);
103 $i++;
104 }
105 if(strpos($line,"TARGETDURATION") !== FALSE) {
106 $return['duration'] = str_replace(array("#EXT-X-TARGETDURATION:","\r","\n"),array("","","",""),$line);
107 }
108 }
109 fclose($handle);
110} else {
111 $retorno = FALSE;
112}
113return $return;
114}
115
116/*
117The results from each file are stored in the relative columns for each video size as a json-encoded string, note that I am storing the minimum information possible in order to minimize read times and make the dB as small as possible. In this step speed doesn't really matter since this is done before serving the file.
118
119Part 4.- Serving the file The system reads the database and serves the correct file for each size.
120
121For this I have an m3u8.domain.com set up which sends all *.m3u8 files to the PHP interpreter so I don't bother with renaming, this serves only the following files:
122
123crossdomain.xml
124240p.m3u8: script for 240p resolution
125480p.m3u8: script for 480p resolution
126720p.m3u8: script for 720p resolution
127aac.m3u8: script for audio-only
128master.m3u8: script for master m3u8
129Each one is its own file because some players had problems if the different bandwidth m3u8s all had the same name.
130
131The master.m3u8 does this:
132*/
133if($sesion !== FALSE) {
134$hls = sql("SELECT A.240p,A.480p,A.720p,A.aac,B.duracion FROM data_contenido_archivos A, data_contenido B WHERE A.id = '".limpia($_GET['i'])."' AND B.id = '".limpia($_GET['i'])."'");
135if($hls['status'] == "OK") {
136 $return = "#EXTM3U\n";
137 if($hls['data'][0]['240p'] != "{}") {
138 $return .= "#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=440000, RESOLUTION=426x240\n";
139 $return .= $domains['m3u8']."/240p.m3u8?i=".$id."&s=".$sesion."\n";
140 }
141 if($hls['data'][0]['480p'] != "{}") {
142 $return .= "#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=912000, RESOLUTION=854x480\n";
143 $return .= $domains['m3u8']."/480p.m3u8?i=".$id."&s=".$sesion."\n";
144 }
145 if($hls['data'][0]['720p'] != "{}") {
146 $return .= "#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=1840000, RESOLUTION=1280x720\n";
147 $return .= $domains['m3u8']."/720p.m3u8?i=".$id."&s=".$sesion."\n";
148 }
149 if($hls['data'][0]['aac'] != "{}") {
150 $retorno .= "#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=64000\n";
151 $retorno .= $domains['m3u8']."/aac.m3u8?i=".$id."&s=".$sesion."\n";
152 }
153}
154}
155header("Content-type: application/x-mpegURL");
156echo $return;
157
158/*
159It, queries the database for all the different resolutions and echos the correct url for each one. If it doesn't exist (set to {} in my scheme), it doesn't get echoed.
160
161Then each res.m3u8 does the following (when called):
162*/
163
164if($sesion) {
165$hls = sql("SELECT A.240p,B.duracion FROM data_contenido_archivos A, data_contenido B WHERE A.id = '".limpia($_GET['i'])."' AND B.id = '".limpia($_GET['i'])."'");
166if($hls['estado'] == "OK") {
167 $data = json_decode($hls['data'][0]['240p'],TRUE);
168 // INICIAMOS EL ARCHIVO
169 $retorno = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-ALLOW-CACHE:YES\n#EXT-X-TARGETDURATION:".$data['duration']."\n";
170 // CALCULAMOS EL VENCIMIENTO (1.5x DURACION DEL VIDEO)
171 $vence = "+".floor($hls['data'][0]['duracion'] / 60)." minutes";
172 foreach($data['data'] as $k=>$v) {
173 $retorno .= "#EXTINF:".$v['inf'].",\n";
174 $retorno .= S3URL("<BUCKET>",$_GET['i']."/240p/segment".$v['ts'].".ts",$vence)."\n";
175 }
176 $retorno .= "#EXT-X-ENDLIST\n";
177}
178}
179header("Content-type: application/x-mpegURL");
180echo $retorno;
181/*
182There's a few things happening here so let me explain:
183
184a.- First the script checks for a valid session, if one doesn't exist, it serves up an m3u8 for a small 10 second video saying "you don't have permission to view this".
185
186b.- If the session checks out, it queries the database and gets all the requisite JSONs. It also queries another table where I store the duration of the file in order to populate the TARGETDURATION string and also to calculate the life of the secure S3 URL to generate. I set the lifetime of the URL to 1.5x the length of the video, it works for me, your experience may be different.
187
188c.- It then iterates through each group from the database, echos the EXTINF and generates a secure URL for each one.*/