-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathradio.php
276 lines (256 loc) · 14.8 KB
/
radio.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
<?php
require_once('mp3info.php');
require_once('config.php');
use wapmorgan\Mp3Info\Mp3Info;
const CHUNK_SIZE = 16384;
class radio{
private $nowPlaying; //какой файл сейчас воспроизводится
private $chunkNumber; //номер чанка этого файла
private $totalChunks; //всего чанков в файле
private $lastUpdate; //последнее обновление данных в памяти
private $ID3Title; //ID3-теги из файла
private $radioHistory; //история воспроизведения
private $fileList; //список файлов (используется при обновлении медиатеки)
public $tracks; //массив файлов (из него выбираются треки для воспроизведения
public $forceNextTrack; //трек, заданный из телеграм-бота
public $deleteTrack; //трек на удаление из телеграм-бота
private $isMaster = false; //данный клиент - мастер (может менять треки при их окончании)
private $withMetadata = false; //клиент запросил метаданные
private $chunkCounter = 0; // общий счетчик чанков (для корректной отправки метаданных внутри потока)
private $intervalBetweenPackets;
private $actualChunkSize;
private $debug = false;
public function __construct($outside = false){
$this->intervalBetweenPackets = (isset($_GET['i'])) ? (float)($_GET['i']) : 1;
$this->actualChunkSize = CHUNK_SIZE * $this->intervalBetweenPackets;
$this->nowPlaying = shmop_open(100000, 'c', 0644, 512);
$this->chunkNumber = shmop_open(100001, 'c', 0644, 4);
$this->lastUpdate = shmop_open(100002, 'c', 0644, 10);
$this->ID3Title = shmop_open(100003, 'c', 0644, 512);
$this->totalChunks = shmop_open(100004, 'c', 0644, 4);
$this->forceNextTrack = shmop_open(100005, 'c', 0644, 512);
$this->radioHistory = shmop_open(100006, 'c', 0644, 1024);
$this->deleteTrack = shmop_open(100007, 'c', 0644, 2);
$this->prerollSent = false;
$this->debug = config::getSetting('debug');
if(isset($_GET['refresh'])){ $this->refreshTrackList();}
else{
if(!isset($_GET['play']) and !$outside){
$_RESPONSE['nowPlaying'] = $this->shmopRead($this->ID3Title);
$_RESPONSE['History'] = unserialize($this->shmopRead($this->radioHistory));
if($this->debug){
$_RESPONSE['File'] = $this->shmopRead($this->nowPlaying);
$_RESPONSE['Chunk'] = $this->shmopRead($this->chunkNumber);
$_RESPONSE['TotalChunks'] = $this->shmopRead($this->totalChunks);
$_RESPONSE['LastUpdated'] = $this->shmopRead($this->lastUpdate);
$_RESPONSE['CurrentTime'] = time();
}
if(isset($_GET['json'])){
header('Content-type: text/json');
echo json_encode($_RESPONSE); exit;
}
else{ include('index.phtml');}
}
else{
$incomingHeaders = getallheaders();
$this->tracks = explode(PHP_EOL,file_get_contents($_SERVER['DOCUMENT_ROOT'].'/radio/fileList.txt'));
if(!$outside){
header('Connection: Keep-Alive');
header('Content-type: audio/mpeg');
header('Content-Transfer-Encoding: binary');
header('Pragma: no-cache');
header('icy-name: Stardisk PHP Radio Server');
header("icy-br: 128");
if(isset($incomingHeaders['Icy-MetaData'])){
header ("icy-metaint: ".$this->actualChunkSize*5);
$this->withMetadata = true;
}
usleep(100000);
flush();
$this->play($this->shmopRead($this->forceNextTrack));
}
}
}
}
private function refreshTrackList($standalone = true){
$this->listFolderFiles(config::getSetting('audioFiles'));
file_put_contents('fileList.txt', substr($this->fileList, 0, -1));
if($standalone){ echo 'done'; exit;}
}
private function listFolderFiles($dir){
$ffs = scandir($dir);
unset($ffs[array_search('.', $ffs, true)]);
unset($ffs[array_search('..', $ffs, true)]);
// prevent empty ordered elements
if (count($ffs) < 1) return;
foreach($ffs as $ff){
if(is_dir($dir.'/'.$ff)) $this->listFolderFiles($dir.'/'.$ff);
else{
$this->fileList .= $dir.'/'.$ff.PHP_EOL;
}
}
}
private function play($nextTrack = false){
//если не задан принудительный переход на другой трек
if(!$nextTrack){
//проверяем, доиграла ли текущая песня
$chunkNumber = $this->shmopRead($this->chunkNumber);
$totalChunks = $this->shmopRead($this->totalChunks);
$lastUpdate = $this->shmopRead($this->lastUpdate);
$time = time();
//осталось меньше 5 чанков до конца
if($totalChunks - $chunkNumber < 5){
if($time - $lastUpdate > 10){ $this->isMaster = true;} //обновлений не было более 10 сек - этот клиент становится мастером
}
//больше 5 чанков до конца
else{
if($time - $lastUpdate > 60){ $this->isMaster = true;} //обновлений не было более 60 сек - этот клиент становится мастером
}
//если клиент - мастер, то он выбирает трек)
if($this->isMaster){
$selected = rand(0, count($this->tracks)-1);
$selectedMP3 = $this->tracks[$selected];
}
//а если не мастер - берем то, что сейчас воспроизводится
else{ $selectedMP3 = $this->shmopRead($this->nowPlaying);}
}
//принудительный переход на трек:
else{
$selectedMP3 = $nextTrack;
$this->shmopWrite($this->forceNextTrack, '');
$chunkNumber = 0;
}
//загружаем данные об этом треке
$mp3file = new Mp3Info($selectedMP3, true);
//определяем название песни по ее id3-тегам
$song = (isset($mp3file->tags['song'])) ? $mp3file->tags['song'] : '';
$artist = (isset($mp3file->tags['artist'])) ? $mp3file->tags['artist'] : '';
if($song and $artist){ $title = $artist.' - '.$song;}
else{
if($locale = config::getSetting('locale')){ setlocale(LC_ALL, $locale);}
$title = basename($selectedMP3);
}
$this->updateHistory($title);
$totalChunks = ceil($mp3file->_fileSize / $this->actualChunkSize); //всего чанков в файле
//открываем файл
$fpOrigin = fopen($selectedMP3, 'rb');
//если клиент - мастер или принудительная смена трека, обновляем общие данные в памяти
if($this->isMaster or $nextTrack){
$this->shmopWrite($this->nowPlaying, $selectedMP3);
$this->shmopWrite($this->chunkNumber, 0);
$this->shmopWrite($this->lastUpdate, time());
$this->shmopWrite($this->ID3Title, $title);
$this->shmopWrite($this->totalChunks, $totalChunks);
$chunkNumber = 0;
}
//если это просто клиент, то начинаем читать файл с того места, где сейчас слушает мастер
else{
if($chunkNumber > 5){ $chunkNumber-=5;}
fseek($fpOrigin, $this->actualChunkSize * $chunkNumber);
}
$exceedSeconds = floor($mp3file->duration - ($totalChunks * $this->intervalBetweenPackets)); //разница между длительностью и числом чанков
if($exceedSeconds > 0){
$oneSecSleepAfterChunkNumber = floor($totalChunks / $exceedSeconds);//определяем после какого чанка (например, после каждого 50-го) вставляем еще один сон 1 сек
}
else{ $oneSecSleepAfterChunkNumber = 0;}
//посылаем файл по чанкам
while(!feof($fpOrigin)){
if($this->shmopRead($this->deleteTrack)){
$this->shmopWrite($this->deleteTrack, '');
fclose($fpOrigin);
usleep(10000);
unlink($this->shmopRead($this->nowPlaying));
usleep(10000);
$this->refreshTrackList(false);
usleep(10000);
$this->play();
return;
}
if(!$this->prerollSent){
$buffer = fread($fpOrigin, $this->actualChunkSize*5);
$chunkNumber+=5;
$this->chunkCounter+=5;
echo $buffer;
if($this->withMetadata){
$debug = ($this->debug) ? ' ('.$chunkNumber.'/'.$this->shmopRead($this->totalChunks).', d: '.floor($mp3file->duration).', s: '.$exceedSeconds.', t: '.$this->chunkCounter.')' : '';
$this->setTitle($title.$debug);
}
$this->prerollSent = true;
}
else{
//читаем чанк
$buffer = fread($fpOrigin, $this->actualChunkSize);
//увеличиваем счетчики чанков
$chunkNumber++; $this->chunkCounter++;
//если до конца остается совсем немного чанков
if($totalChunks - $chunkNumber < 5){
//проверяем его размер
$bufferLength = strlen($buffer);
//если размер чанка меньше стандартного, добиваем недостающее нулями, ибо все чанки должны быть одинакового размера
if($bufferLength < $this->actualChunkSize){ $buffer .= str_repeat("\xff", $this->actualChunkSize - $bufferLength);}
}
//посылаем чанк клиенту
echo $buffer;
flush();
//если номер чанка кратен 5, обновляем статус на сервере и посылаем метаданные
//if($this->chunkCounter % 5 == 0){
//если статус радио не обновлялся более 5 секунд, теперь этот клиент - мастер
if(!$this->isMaster and (time() - $this->shmopRead($this->lastUpdate) > 5)){ $this->isMaster = true;}
//если клиент - мастер -он обновляет общие данные
if($this->isMaster){
$this->shmopWrite($this->chunkNumber, $chunkNumber);
$this->shmopWrite($this->lastUpdate, time());
}
//если клиент запросил метаданные, посылаем их ему
if($this->chunkCounter % 5 == 0 and $this->withMetadata){
$debug = ($this->debug) ? ' ('.$chunkNumber.'/'.$this->shmopRead($this->totalChunks).', d: '.floor($mp3file->duration).', s: '.$exceedSeconds.', t: '.$this->chunkCounter.')' : '';
$this->setTitle($title.$debug);
}
//}
//спим секунду до отправки следующего чанка
if($chunkNumber > 0 and $oneSecSleepAfterChunkNumber > 0 and $chunkNumber % $oneSecSleepAfterChunkNumber == 0){ sleep(2); $exceedSeconds--;}
else {usleep($this->intervalBetweenPackets * 1000000);}
//если запрошен переход на следующий трек
if($nextTrack = $this->shmopRead($this->forceNextTrack)){
//закрываем текущий файл и играем запрошенный
fclose($fpOrigin);
$this->play($nextTrack);
return;
}
}
}
//дослали файл чанками - закрыли
fclose($fpOrigin);
//если сон после воспроизведения трека больше нуля, то спим это время, т.к. кол-во чанков обычно не равно числу секунд файла и отличается на 3-9
// if($exceedSeconds > 0) sleep($exceedSeconds);
//играем следующий трек
$this->play();
}
//отправка метаданных
//они выглядят так: [1 байт, означающий размер метаданных, число байт / 16, например для 48 байт он должен быть 0x03]StreamTitle='автор - песня';
private function setTitle($title){
$title = "StreamTitle='$title';";
$titleLength = strlen($title);
$requiredLength = ceil($titleLength / 16); //определяем размер метаданных
$title .= str_repeat("\0", $requiredLength * 16 - $titleLength); //если не хватает до кратного 16 - добиваем нулями
echo pack('c', $requiredLength).$title; //пресловутый байт в чистом виде
}
private function updateHistory($trackName){
$history = unserialize($this->shmopRead($this->radioHistory));
if(!$history){ $history = [];}
if(end($history) != $trackName)
$history[] = $trackName;
if(count($history) > 10){ unset($history[0]);}
$this->shmopWrite($this->radioHistory, serialize(array_values($history)));
}
private function shmopRead($block){
$size = shmop_size($block);
return trim(shmop_read($block,0,$size));
}
public function shmopWrite($block, $data){
$size = shmop_size($block);
$emptyBytes = $size - strlen($data);
if($emptyBytes > 0){ $data .= str_repeat(' ', $emptyBytes);}
shmop_write($block, $data, 0);
}
}