feat: add hls play

This commit is contained in:
langhuihui
2024-11-15 14:05:58 +08:00
parent 3ede6ec08f
commit f548273169
28 changed files with 93927 additions and 22 deletions
+4
View File
@@ -17,3 +17,7 @@ mp4:
onsub:
pull:
^vod/(.+)$: $1
hls:
onpub:
transform:
.* : 5s x 3
+6 -1
View File
@@ -55,7 +55,12 @@ func (A *ADTS) Demux(ctx codec.ICodecCtx) (any, error) {
}
func (A *ADTS) Mux(ctx codec.ICodecCtx, frame *AVFrame) {
aacCtx := ctx.GetBase().(*codec.AACCtx)
A.DTS = frame.Timestamp * 90 / time.Millisecond
aacCtx, ok := ctx.GetBase().(*codec.AACCtx)
if !ok {
A.Append(frame.Raw.(util.Memory).Buffers...)
return
}
A.InitRecycleIndexes(1)
adts := A.NextN(7)
raw := frame.Raw.(util.Memory)
+2
View File
@@ -155,6 +155,8 @@ func (a *AnnexB) Demux(codecCtx codec.ICodecCtx) (ret any, err error) {
}
func (a *AnnexB) Mux(codecCtx codec.ICodecCtx, frame *AVFrame) {
a.DTS = frame.Timestamp * 90 / time.Millisecond
a.PTS = a.DTS + frame.CTS*90/time.Millisecond
a.InitRecycleIndexes(0)
delimiter2 := codec.NALU_Delimiter2[:]
a.AppendOne(delimiter2)
+38
View File
@@ -0,0 +1,38 @@
<html>
<head>
<title>Hls.js demo - basic usage</title>
</head>
<body>
<script src="hls.js"></script>
<center>
<h1>Hls.js demo - basic usage</h1>
<video height="600" id="video" controls></video>
</center>
<script>
var video = document.getElementById('video');
if (Hls.isSupported()) {
var hls = new Hls({
debug: true,
});
hls.loadSource('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
video.muted = true;
video.play();
});
}
// hls.js is not supported on platforms that do not have Media Source Extensions (MSE) enabled.
// When the browser has built-in HLS support (check using `canPlayType`), we can provide an HLS manifest (i.e. .m3u8 URL) directly to the video element through the `src` property.
// This is using the built-in support of the plain video element, without using hls.js.
else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
video.addEventListener('canplay', function () {
video.play();
});
}
</script>
</body>
</html>
+68
View File
@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title></title>
</head>
<body>
<script src="hls.js"></script>
<video id="video" controls></video>
<script>
function parseQuery(queryString) {
var query = {};
var pairs = (
queryString[0] === '?' ? queryString.slice(1) : queryString
).split('&');
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split('=');
query[decodeURIComponent(pair[0])] = decodeURIComponent(
pair[1] || ''
);
}
return query;
}
/* get stream from query string */
function getParameterByName(name) {
var query = parseQuery(window.location.search);
return query.hasOwnProperty(name) ? query[name] : undefined;
}
var stream =
getParameterByName('stream') ||
'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
</script>
<script>
if (Hls.isSupported()) {
var video = document.getElementById('video');
var hls = new Hls();
hls.loadSource(stream);
hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
video.muted = true;
video.play();
});
}
</script>
<script>
var video = document.getElementById('video');
window.onload = function () {
var i = 0;
var el = document.getElementById('update');
function foo() {
i++;
el.innerHTML =
'animation:' +
i +
',decoded:' +
video.webkitDecodedFrameCount +
',dropped:' +
video.webkitDroppedFrameCount;
window.requestAnimationFrame(foo);
}
foo();
};
</script>
<div id="update"></div>
</body>
</html>
+729
View File
@@ -0,0 +1,729 @@
/* eslint no-var: 0, camelcase: 0 */
var eventLeftMargin = 180;
var eventRightMargin = 0;
function canvasLoadEventUpdate(canvas, minTime, maxTime, events) {
var event;
var start;
var ctx = canvas.getContext('2d');
for (var i = 0, y_offset = 20; i < events.length; i++) {
event = events[i];
start = event.time;
// var end = event.time + event.duration + event.latency;
if (start >= minTime && start <= maxTime) {
y_offset += 20;
}
}
canvas.height = y_offset;
ctx.fillStyle = 'green';
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, eventLeftMargin, canvas.height);
ctx.fillRect(
canvas.width - eventRightMargin,
0,
eventRightMargin,
canvas.height
);
ctx.globalAlpha = 1;
// draw legend
var x_offset = 5;
ctx.font = '12px Arial';
var legend = 'load event';
ctx.fillStyle = 'black';
ctx.fillText(legend, x_offset, 15);
x_offset = eventLeftMargin + 5;
legend = 'start - end';
ctx.fillStyle = 'black';
ctx.fillText(legend, x_offset, 15);
x_offset += ctx.measureText(legend).width + 5;
legend = '[latency';
ctx.fillStyle = 'orange';
ctx.fillText(legend, x_offset, 15);
x_offset += ctx.measureText(legend).width + 5;
legend = 'loading';
ctx.fillStyle = 'green';
ctx.fillText(legend, x_offset, 15);
x_offset += ctx.measureText(legend).width + 5;
legend = 'parsing';
ctx.fillStyle = 'blue';
ctx.fillText(legend, x_offset, 15);
x_offset += ctx.measureText(legend).width + 5;
legend = 'appending]';
ctx.fillStyle = 'red';
ctx.fillText(legend, x_offset, 15);
x_offset += ctx.measureText(legend).width + 5;
legend = 'size bitrate';
ctx.fillStyle = 'black';
ctx.fillText(legend, x_offset, 15);
x_offset += ctx.measureText(legend).width + 5;
for (i = 0, y_offset = 20; i < events.length; i++) {
event = events[i];
start = Math.round(event.time);
// var end = Math.round(event.time + event.duration + event.latency);
if (start >= minTime && start <= maxTime) {
canvasDrawLoadEvent(ctx, y_offset, event, minTime, maxTime);
y_offset += 20;
}
}
}
function canvasVideoEventUpdate(canvas, minTime, maxTime, events) {
var event;
var start;
var ctx = canvas.getContext('2d');
for (var i = 0, y_offset = 20; i < events.length; i++) {
event = events[i];
start = event.time;
// end = event.time;
if (start >= minTime && start <= maxTime) {
y_offset += 20;
}
}
canvas.height = y_offset;
ctx.fillStyle = 'green';
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, eventLeftMargin, canvas.height);
ctx.fillRect(
canvas.width - eventRightMargin,
0,
eventRightMargin,
canvas.height
);
ctx.globalAlpha = 1;
// draw legend
var x_offset = 5;
ctx.font = '12px Arial';
var legend = 'video event';
ctx.fillStyle = 'black';
ctx.fillText(legend, x_offset, 15);
x_offset = eventLeftMargin + 5;
legend = 'time';
ctx.fillStyle = 'black';
ctx.fillText(legend, x_offset, 15);
x_offset += ctx.measureText(legend).width + 5;
legend = '[duration]';
ctx.fillStyle = 'blue';
ctx.fillText(legend, x_offset, 15);
for (i = 0, y_offset = 20; i < events.length; i++) {
event = events[i];
start = Math.round(event.time);
// end = Math.round(event.time);
if (start >= minTime && start <= maxTime) {
canvasDrawVideoEvent(ctx, y_offset, event, minTime, maxTime);
y_offset += 20;
}
}
}
function canvasBufferWindowUpdate(canvas, minTime, maxTime, focusTime, events) {
var ctx = canvas.getContext('2d');
var minTimeBuffer;
var minTimePos;
var focusTimeBuffer;
var focusTimePos;
var bufferChartStart = eventLeftMargin;
var bufferChartWidth = ctx.canvas.width - eventLeftMargin - eventRightMargin;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (events.length === 0) {
return;
}
ctx.fillStyle = 'green';
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, eventLeftMargin, canvas.height);
ctx.globalAlpha = 1;
// draw legend
var x_offset = 5;
var y_offset = 0;
ctx.font = '15px Arial';
var maxBuffer = 0;
var firstEventIdx = -1;
var focusEventIdx = -1;
var event;
for (var i = 0; i < events.length; i++) {
event = events[i];
maxBuffer = Math.max(maxBuffer, event.buffer + event.pos);
if (firstEventIdx === -1 && event.time >= minTime) {
firstEventIdx = Math.max(0, i - 1);
}
if (focusEventIdx === -1 && event.time >= focusTime) {
focusEventIdx = Math.max(0, i - 1);
}
}
// compute position and buffer length at pos minTime using linear approximation
if (firstEventIdx + 1 < events.length) {
minTimePos =
events[firstEventIdx].pos +
((minTime - events[firstEventIdx].time) *
(events[firstEventIdx + 1].pos - events[firstEventIdx].pos)) /
(events[firstEventIdx + 1].time - events[firstEventIdx].time);
minTimeBuffer =
minTimePos +
events[firstEventIdx].buffer +
((minTime - events[firstEventIdx].time) *
(events[firstEventIdx + 1].buffer - events[firstEventIdx].buffer)) /
(events[firstEventIdx + 1].time - events[firstEventIdx].time);
} else {
minTimeBuffer = 0;
minTimePos = 0;
}
// compute position and buffer length at pos focusTime using linear approximation
if (focusEventIdx + 1 < events.length) {
focusTimePos =
events[focusEventIdx].pos +
((focusTime - events[focusEventIdx].time) *
(events[focusEventIdx + 1].pos - events[focusEventIdx].pos)) /
(events[focusEventIdx + 1].time - events[focusEventIdx].time);
focusTimeBuffer =
events[focusEventIdx].buffer +
((focusTime - events[focusEventIdx].time) *
(events[focusEventIdx + 1].buffer - events[focusEventIdx].buffer)) /
(events[focusEventIdx + 1].time - events[focusEventIdx].time);
} else {
focusTimePos = 0;
focusTimeBuffer = 0;
}
maxBuffer *= 1.1;
y_offset += 15;
var legend = 'play pos/buffer zoomed';
ctx.fillStyle = 'black';
ctx.fillText(legend, x_offset, y_offset);
y_offset += 15;
legend = '[' + minTime + ',' + maxTime + ']';
ctx.fillText(legend, x_offset, y_offset);
y_offset += 15;
legend = 'focus time:' + focusTime + ' ms';
ctx.fillText(legend, x_offset, y_offset);
y_offset += 15;
legend = 'focus position:' + Math.round(focusTimePos) + ' ms';
ctx.fillText(legend, x_offset, y_offset);
y_offset += 15;
legend = 'focus buffer:' + Math.round(focusTimeBuffer) + ' ms';
ctx.fillText(legend, x_offset, y_offset);
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.moveTo(bufferChartStart, ctx.canvas.height);
ctx.lineTo(
bufferChartStart,
ctx.canvas.height * (1 - minTimeBuffer / maxBuffer)
);
for (var j = firstEventIdx + 1; j < events.length; j++) {
event = events[j];
x_offset =
bufferChartStart +
(bufferChartWidth * (event.time - minTime)) / (maxTime - minTime);
y_offset = ctx.canvas.height * (1 - (event.buffer + event.pos) / maxBuffer);
ctx.lineTo(x_offset, y_offset);
}
ctx.lineTo(x_offset, canvas.height);
ctx.fill();
ctx.fillStyle = 'brown';
ctx.beginPath();
ctx.moveTo(bufferChartStart, ctx.canvas.height);
ctx.lineTo(
bufferChartStart,
ctx.canvas.height * (1 - minTimePos / maxBuffer)
);
for (var k = firstEventIdx + 1; k < events.length; k++) {
event = events[k];
x_offset =
bufferChartStart +
(bufferChartWidth * (event.time - minTime)) / (maxTime - minTime);
y_offset = ctx.canvas.height * (1 - event.pos / maxBuffer);
ctx.lineTo(x_offset, y_offset);
}
ctx.lineTo(x_offset, canvas.height);
ctx.fill();
ctx.fillStyle = 'white';
ctx.fillRect(
canvas.width - eventRightMargin,
0,
eventRightMargin,
canvas.height
);
ctx.fillStyle = 'green';
ctx.globalAlpha = 0.5;
ctx.fillRect(
canvas.width - eventRightMargin,
0,
eventRightMargin,
canvas.height
);
ctx.globalAlpha = 1;
ctx.fillStyle = 'black';
x_offset =
bufferChartStart +
(bufferChartWidth * (focusTime - minTime)) / (maxTime - minTime);
ctx.moveTo(x_offset, ctx.canvas.height);
y_offset =
ctx.canvas.height * (1 - (focusTimePos + focusTimeBuffer) / maxBuffer);
ctx.lineTo(x_offset, y_offset);
ctx.stroke();
}
function canvasBufferTimeRangeUpdate(
canvas,
minTime,
maxTime,
windowMinTime,
windowMaxTime,
events
) {
var ctx = canvas.getContext('2d');
var bufferChartStart = eventLeftMargin;
var bufferChartWidth = ctx.canvas.width - eventLeftMargin - eventRightMargin;
var x_offset = 0;
var y_offset = 0;
var event;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'green';
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, eventLeftMargin, canvas.height);
ctx.fillRect(
canvas.width - eventRightMargin,
0,
eventRightMargin,
canvas.height
);
ctx.globalAlpha = 1;
x_offset = 5;
y_offset = 15;
var legend = 'play pos/buffer';
ctx.fillStyle = 'black';
ctx.font = '15px Arial';
ctx.fillText(legend, x_offset, y_offset);
if (events.length === 0) {
return;
}
var maxBuffer = 0;
for (var i = 0; i < events.length; i++) {
maxBuffer = Math.max(maxBuffer, events[i].buffer + events[i].pos);
}
y_offset += 15;
legend = 'last pos:' + events[events.length - 1].pos + ' ms';
ctx.fillText(legend, x_offset, y_offset);
y_offset += 15;
legend = 'last buffer:' + events[events.length - 1].buffer + ' ms';
ctx.fillText(legend, x_offset, y_offset);
y_offset += 15;
legend = 'max buffer:' + maxBuffer + ' ms';
ctx.fillText(legend, x_offset, y_offset);
y_offset += 15;
legend = 'nb samples:' + events.length;
ctx.fillText(legend, x_offset, y_offset);
maxBuffer *= 1.1;
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.moveTo(bufferChartStart, ctx.canvas.height);
for (var j = 0; j < events.length; j++) {
event = events[j];
x_offset =
bufferChartStart +
(bufferChartWidth * (event.time - minTime)) / (maxTime - minTime);
y_offset = ctx.canvas.height * (1 - (event.buffer + event.pos) / maxBuffer);
ctx.lineTo(x_offset, y_offset);
}
ctx.lineTo(x_offset, canvas.height);
ctx.fill();
ctx.fillStyle = 'brown';
ctx.beginPath();
ctx.moveTo(bufferChartStart, ctx.canvas.height);
for (var k = 0; k < events.length; k++) {
event = events[k];
x_offset =
bufferChartStart +
(bufferChartWidth * (event.time - minTime)) / (maxTime - minTime);
y_offset = ctx.canvas.height * (1 - event.pos / maxBuffer);
ctx.lineTo(x_offset, y_offset);
}
ctx.lineTo(x_offset, canvas.height);
ctx.fill();
ctx.globalAlpha = 0.7;
ctx.fillStyle = 'grey';
var x_start = bufferChartStart;
var x_w =
(bufferChartWidth * (windowMinTime - minTime)) / (maxTime - minTime);
ctx.fillRect(x_start, 0, x_w, canvas.height);
x_start =
bufferChartStart +
(bufferChartWidth * (windowMaxTime - minTime)) / (maxTime - minTime);
x_w = canvas.width - x_start - eventRightMargin;
ctx.fillRect(x_start, 0, x_w, canvas.height);
ctx.globalAlpha = 1;
}
function canvasBitrateEventUpdate(
canvas,
minTime,
maxTime,
windowMinTime,
windowMaxTime,
levelEvents,
bitrateEvents
) {
var ctx = canvas.getContext('2d');
var bufferChartStart = eventLeftMargin;
var bufferChartWidth = ctx.canvas.width - eventLeftMargin - eventRightMargin;
var x_offset = 0;
var y_offset = 0;
var event;
var maxLevel;
var minLevel;
var sumLevel;
var maxBitrate;
var minBitrate;
var sumDuration;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (levelEvents.length === 0 || bitrateEvents.length === 0) {
return;
}
maxBitrate = minBitrate = bitrateEvents[0].bitrate;
sumLevel = sumDuration = 0;
for (var i = 0; i < bitrateEvents.length; i++) {
sumLevel += bitrateEvents[i].duration * bitrateEvents[i].level;
sumDuration += bitrateEvents[i].duration;
maxBitrate = Math.max(maxBitrate, bitrateEvents[i].bitrate);
minBitrate = Math.min(minBitrate, bitrateEvents[i].bitrate);
}
maxLevel = minLevel = levelEvents[0].id;
for (var j = 0; j < levelEvents.length; j++) {
maxLevel = Math.max(maxLevel, levelEvents[j].id);
minLevel = Math.min(minLevel, levelEvents[j].id);
}
ctx.fillStyle = 'green';
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, eventLeftMargin, canvas.height);
ctx.fillRect(
canvas.width - eventRightMargin,
0,
eventRightMargin,
canvas.height
);
ctx.globalAlpha = 1;
x_offset = 5;
y_offset = 0;
ctx.fillStyle = 'black';
ctx.font = '15px Arial';
y_offset += 15;
var legend =
'last bitrate:' +
(bitrateEvents[bitrateEvents.length - 1].bitrate / 1000).toFixed(2) +
'Mb/s';
ctx.fillText(legend, x_offset, y_offset);
y_offset += 15;
legend = 'min bitrate:' + (minBitrate / 1000).toFixed(2) + 'Mb/s';
ctx.fillText(legend, x_offset, y_offset);
y_offset += 15;
legend = 'max bitrate:' + (maxBitrate / 1000).toFixed(2) + 'Mb/s';
ctx.fillText(legend, x_offset, y_offset);
y_offset += 15;
legend =
'min/last/max level:' +
minLevel +
'/' +
levelEvents[levelEvents.length - 1].id +
'/' +
maxLevel;
ctx.fillText(legend, x_offset, y_offset);
y_offset += 15;
legend = 'nb level switch:' + (levelEvents.length - 1);
ctx.fillText(legend, x_offset, y_offset);
y_offset += 15;
legend = 'average level:' + (sumLevel / sumDuration).toFixed(2);
ctx.fillText(legend, x_offset, y_offset);
maxBitrate *= 1.1;
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.moveTo(bufferChartStart, ctx.canvas.height);
for (var k = 0; k < bitrateEvents.length; k++) {
event = bitrateEvents[k];
x_offset =
bufferChartStart +
(bufferChartWidth * (event.time - minTime)) / (maxTime - minTime);
y_offset = ctx.canvas.height * (1 - event.bitrate / maxBitrate);
ctx.lineTo(x_offset, y_offset);
}
ctx.lineTo(bufferChartStart + bufferChartWidth, y_offset);
ctx.stroke();
ctx.strokeStyle = 'black';
ctx.beginPath();
x_offset = bufferChartStart;
y_offset = ctx.canvas.height;
ctx.moveTo(x_offset, y_offset);
for (var l = 0; l < levelEvents.length; l++) {
event = levelEvents[l];
x_offset =
bufferChartStart +
(bufferChartWidth * (event.time - minTime)) / (maxTime - minTime);
ctx.lineTo(x_offset, y_offset);
y_offset = ctx.canvas.height * (1 - event.bitrate / maxBitrate);
ctx.lineTo(x_offset, y_offset);
}
ctx.lineTo(bufferChartStart + bufferChartWidth, y_offset);
ctx.stroke();
ctx.globalAlpha = 0.7;
ctx.fillStyle = 'grey';
var x_start = bufferChartStart;
var x_w =
(bufferChartWidth * (windowMinTime - minTime)) / (maxTime - minTime);
ctx.fillRect(x_start, 0, x_w, canvas.height);
x_start =
bufferChartStart +
(bufferChartWidth * (windowMaxTime - minTime)) / (maxTime - minTime);
x_w = canvas.width - x_start - eventRightMargin;
ctx.fillRect(x_start, 0, x_w, canvas.height);
ctx.globalAlpha = 1;
}
function canvasDrawLoadEvent(ctx, yoffset, event, minTime, maxTime) {
var legend;
var offset;
var x_start;
var x_w;
var networkChartStart = eventLeftMargin;
var networkChartWidth = ctx.canvas.width - eventLeftMargin - eventRightMargin;
var tend = Math.round(event.time + event.duration + event.latency);
// draw start
ctx.fillStyle = 'black';
ctx.font = '12px Arial';
legend = Math.round(event.time);
offset = ctx.measureText(legend).width + 5;
x_start =
networkChartStart -
offset +
(networkChartWidth * (event.time - minTime)) / (maxTime - minTime);
ctx.fillText(legend, x_start, yoffset + 12);
// draw latency rectangle
ctx.fillStyle = 'orange';
x_start =
networkChartStart +
(networkChartWidth * (event.time - minTime)) / (maxTime - minTime);
x_w = (networkChartWidth * event.latency) / (maxTime - minTime);
ctx.fillRect(x_start, yoffset, x_w, 15);
// draw download rectangle
ctx.fillStyle = 'green';
x_start =
networkChartStart +
(networkChartWidth * (event.time + event.latency - minTime)) /
(maxTime - minTime);
x_w = (networkChartWidth * event.load) / (maxTime - minTime);
ctx.fillRect(x_start, yoffset, x_w, 15);
if (event.parsing) {
// draw parsing rectangle
ctx.fillStyle = 'blue';
x_start =
networkChartStart +
(networkChartWidth *
(event.time + event.latency + event.load - minTime)) /
(maxTime - minTime);
x_w = (networkChartWidth * event.parsing) / (maxTime - minTime);
ctx.fillRect(x_start, yoffset, x_w, 15);
if (event.buffer) {
// draw buffering rectangle
ctx.fillStyle = 'red';
x_start =
networkChartStart +
(networkChartWidth *
(event.time + event.latency + event.load + event.parsing - minTime)) /
(maxTime - minTime);
x_w = (networkChartWidth * event.buffer) / (maxTime - minTime);
ctx.fillRect(x_start, yoffset, x_w, 15);
}
}
// draw end time
ctx.fillStyle = 'black';
ctx.font = '12px Arial';
legend = tend;
x_start += x_w + 5;
ctx.fillText(legend, x_start, yoffset + 12);
x_start += ctx.measureText(legend).width + 5;
legend = '[' + Math.round(event.latency);
ctx.fillStyle = 'orange';
ctx.fillText(legend, x_start, yoffset + 12);
x_start += ctx.measureText(legend).width + 5;
legend = Math.round(event.load);
if (!event.parsing) {
legend += ']';
}
ctx.fillStyle = 'green';
ctx.fillText(legend, x_start, yoffset + 12);
x_start += ctx.measureText(legend).width + 5;
if (event.parsing) {
legend = Math.round(event.parsing);
if (!event.buffer) {
legend += ']';
}
ctx.fillStyle = 'blue';
ctx.fillText(legend, x_start, yoffset + 12);
x_start += ctx.measureText(legend).width + 5;
if (event.buffer) {
legend = Math.round(event.buffer) + ']';
ctx.fillStyle = 'red';
ctx.fillText(legend, x_start, yoffset + 12);
x_start += ctx.measureText(legend).width + 5;
}
}
if (event.size) {
if (event.size > 1000 * 1000) {
legend = (event.size / 1000000).toFixed(1) + 'MB';
} else {
legend = Math.round(event.size / 1000) + 'kB';
}
ctx.fillStyle = 'black';
ctx.fillText(legend, x_start, yoffset + 12);
x_start += ctx.measureText(legend).width + 5;
}
if (event.bw) {
if (event.bw > 1000) {
legend = (event.bw / 1000).toFixed(1) + 'Mbps';
} else {
legend = event.bw + ' kbps';
}
ctx.fillStyle = 'black';
ctx.fillText(legend, x_start, yoffset + 12);
x_start += ctx.measureText(legend).width + 5;
}
// draw event name
ctx.fillStyle = 'black';
ctx.font = '15px Arial';
legend = event.type;
if (event.id2 !== undefined) {
legend += ' ' + event.id2;
}
if (event.id3 !== undefined) {
legend += '/' + event.id3;
}
if (event.id !== undefined) {
if (event.type.indexOf('fragment') !== -1) {
legend += ' @';
}
legend += ' ' + event.id;
}
if (event.start !== undefined) {
legend += ' [' + event.start + ',' + event.end + ']';
}
ctx.fillText(legend, 5, yoffset + 15);
}
function canvasDrawVideoEvent(ctx, yoffset, event, minTime, maxTime) {
var legend;
var offset;
var x_start;
var x_w;
var networkChartStart = eventLeftMargin;
var networkChartWidth = ctx.canvas.width - eventLeftMargin - eventRightMargin;
// draw event name
ctx.fillStyle = 'black';
ctx.font = '15px Arial';
legend = event.type;
if (event.name !== undefined) {
legend += ':' + event.name;
}
ctx.fillText(legend, 5, yoffset + 15);
// draw start time
ctx.fillStyle = 'black';
ctx.font = '12px Arial';
legend = Math.round(event.time);
offset = ctx.measureText(legend).width + 5;
x_start =
networkChartStart -
offset +
(networkChartWidth * (event.time - minTime)) / (maxTime - minTime);
ctx.fillText(legend, x_start, yoffset + 12);
// draw event rectangle
x_start =
networkChartStart +
(networkChartWidth * (event.time - minTime)) / (maxTime - minTime);
if (event.duration) {
x_w = (networkChartWidth * event.duration) / (maxTime - minTime);
} else {
x_w = 1;
}
ctx.fillRect(x_start, yoffset, x_w, 15);
if (event.duration) {
// draw end time
ctx.fillStyle = 'black';
ctx.font = '12px Arial';
legend = Math.round(event.time + event.duration);
x_start += x_w + 5;
ctx.fillText(legend, x_start, yoffset + 12);
x_start += ctx.measureText(legend).width + 5;
legend = '[' + Math.round(event.duration) + ']';
ctx.fillStyle = 'blue';
ctx.fillText(legend, x_start, yoffset + 12);
}
}
@@ -0,0 +1,293 @@
import Chart from 'chart.js';
// Modify horizontalBar so that each dataset (fragments, timeRanges) draws on the same row (level, track or buffer)
Chart.controllers.horizontalBar.prototype.calculateBarValuePixels = function (
datasetIndex,
index,
options
) {
const chart = this.chart;
const scale = this._getValueScale();
const datasets = chart.data.datasets;
if (!datasets) {
throw new Error(`Chart datasets are ${datasets}`);
}
scale._parseValue = scaleParseValue;
const obj = datasets[datasetIndex].data[index];
const value = scale._parseValue(obj);
const start =
value.start === undefined
? 0
: value.max >= 0 && value.min >= 0
? value.min
: value.max;
const length =
value.start === undefined
? value.end
: value.max >= 0 && value.min >= 0
? value.max - value.min
: value.min - value.max;
const base = scale.getPixelForValue(start);
const head = scale.getPixelForValue(start + length);
const size = head - base;
return {
size: size,
base: base,
head: head,
center: head + size / 2,
};
};
Chart.controllers.horizontalBar.prototype.calculateBarIndexPixels = function (
datasetIndex,
index,
ruler,
options
) {
const rowHeight = options.barThickness;
const size = rowHeight * options.categoryPercentage;
const center = ruler.start + (datasetIndex * rowHeight + rowHeight / 2);
return {
base: center - size / 2,
head: center + size / 2,
center,
size,
};
};
Chart.controllers.horizontalBar.prototype.draw = function () {
const rects = this.getMeta().data;
const len = rects.length;
const dataset = this.getDataset();
if (len !== dataset.data.length) {
// View does not match dataset (wait for redraw)
return;
}
const chart = this.chart;
const scale = this._getValueScale();
scale._parseValue = scaleParseValue;
const ctx: CanvasRenderingContext2D = chart.ctx;
const chartArea: { left; top; right; bottom } = chart.chartArea;
Chart.helpers.canvas.clipArea(ctx, chartArea);
if (!this.lineHeight) {
this.lineHeight =
Math.ceil(ctx.measureText('0').actualBoundingBoxAscent) + 2;
}
const lineHeight = this.lineHeight;
let range = 0;
for (let i = 0; i < len; ++i) {
const rect = rects[i];
const view = rect._view;
if (!intersects(view.base, view.x, chartArea.left, chartArea.right)) {
// Do not draw elements outside of the chart's viewport
continue;
}
const obj = dataset.data[i];
const val = scale._parseValue(obj);
if (!isNaN(val.min) && !isNaN(val.max)) {
const { dataType } = obj;
let { stats } = obj;
const isPart = dataType === 'part';
const isFragmentHint = dataType === 'fragmentHint';
const isFragment = dataType === 'fragment' || isPart || isFragmentHint;
const isCue = dataType === 'cue';
if (isCue) {
view.y += view.height * 0.5 * (i % 2) - view.height * 0.25;
} else if (isPart) {
view.height -= 22;
}
const bounds = boundingRects(view);
const drawText = bounds.w > lineHeight * 1.5 && !isFragmentHint;
if (isFragment || isCue) {
if (drawText) {
view.borderWidth = 1;
if (i === 0) {
view.borderSkipped = false;
}
} else {
range =
range ||
scale.getValueForPixel(chartArea.right) -
scale.getValueForPixel(chartArea.left);
if (range > 300 || isCue) {
view.borderWidth = 0;
}
}
if (isFragmentHint) {
view.borderWidth = 0;
view.backgroundColor = 'rgba(0, 0, 0, 0.1)';
} else {
view.backgroundColor = `rgba(0, 0, 0, ${0.05 + (i % 2) / 12})`;
}
}
rect.draw();
if (isFragment) {
if (!stats) {
stats = {};
}
if (isPart) {
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fillRect(bounds.x, bounds.y, bounds.w, bounds.h);
}
if (stats.aborted) {
ctx.fillStyle = 'rgba(100, 0, 0, 0.3)';
ctx.fillRect(bounds.x, bounds.y, bounds.w, bounds.h);
}
if (stats.loaded && stats.total) {
ctx.fillStyle = 'rgba(50, 20, 100, 0.3)';
ctx.fillRect(
bounds.x,
bounds.y,
(bounds.w * stats.loaded) / stats.total,
bounds.h
);
}
} else if (isCue) {
if (obj.active) {
ctx.fillStyle = 'rgba(100, 100, 10, 0.4)';
ctx.fillRect(bounds.x, bounds.y, bounds.w, bounds.h);
}
}
if (drawText) {
const start = val.start; // obj.start;
ctx.fillStyle = 'rgb(0, 0, 0)';
if (stats) {
const snBounds = Object.assign({}, bounds);
if (obj.cc) {
const ccLabel = `cc:${obj.cc}`;
const ccWidth = Math.min(
ctx.measureText(ccLabel).width + 2,
snBounds.w / 2 - 2
);
if (ccWidth) {
ctx.fillText(
ccLabel,
snBounds.x + 2,
snBounds.y + lineHeight,
snBounds.w / 2 - 4
);
snBounds.x += ccWidth;
snBounds.w -= ccWidth;
}
}
const snLabel = isPart ? `part: ${obj.index}` : `sn: ${obj.sn}`;
const textWidth = Math.min(
ctx.measureText(snLabel).width + 2,
snBounds.w - 2
);
ctx.fillText(
snLabel,
snBounds.x + snBounds.w - textWidth,
snBounds.y + lineHeight,
snBounds.w - 4
);
}
if (isCue) {
const strLength = Math.min(
30,
Math.ceil(bounds.w / (lineHeight / 3))
);
ctx.fillText(
('' + obj.content).slice(0, strLength),
bounds.x + 2,
bounds.y + bounds.h - 3,
bounds.w - 5
);
} else if (!isPart) {
const float = start !== (start | 0);
const fixedDigits = float
? Math.min(5, Math.max(1, Math.floor(bounds.w / 10 - 1)))
: 0;
const startString = hhmmss(start, fixedDigits);
ctx.fillText(
startString,
bounds.x + 2,
bounds.y + bounds.h - 3,
bounds.w - 5
);
}
}
}
}
Chart.helpers.canvas.unclipArea(chart.ctx);
};
export function applyChartInstanceOverrides(chart) {
Object.keys(chart.scales).forEach((axis) => {
const scale = chart.scales![axis];
scale._parseValue = scaleParseValue;
});
}
function scaleParseValue(value: number[] | any) {
if (value === undefined) {
console.warn('Chart values undefined (update chart)');
return {};
}
let start;
let end;
let min;
let max;
if (Array.isArray(value)) {
start = +this.getRightValue(value[0]);
end = +this.getRightValue(value[1]);
min = Math.min(start, end);
max = Math.max(start, end);
} else {
start = +this.getRightValue(value.start);
if ('end' in value) {
end = +this.getRightValue(value.end);
} else {
end = +this.getRightValue(value.start + value.duration);
}
min = Math.min(start, end);
max = Math.max(start, end);
}
return {
min,
max,
start,
end,
};
}
function intersects(x1, x2, x3, x4) {
return x2 > x3 && x1 < x4;
}
function boundingRects(vm) {
const half = vm.height / 2;
const left = Math.min(vm.x, vm.base);
const right = Math.max(vm.x, vm.base);
const top = vm.y - half;
const bottom = vm.y + half;
return {
x: left,
y: top,
w: right - left,
h: bottom - top,
};
}
export function hhmmss(value, fixedDigits) {
const h = (value / 3600) | 0;
const m = ((value / 60) | 0) % 60;
const s = value % 60;
return `${h}:${pad(m, 2)}:${pad(
s.toFixed(fixedDigits),
fixedDigits ? fixedDigits + 3 : 2
)}`.replace(/^(?:0+:?)*(\d.*?)(?:\.0*)?$/, '$1');
}
function pad(str, length) {
str = '' + str;
while (str.length < length) {
str = '0' + str;
}
return str;
}
+950
View File
@@ -0,0 +1,950 @@
import Chart from 'chart.js';
import { applyChartInstanceOverrides, hhmmss } from './chartjs-horizontal-bar';
import { Fragment } from '../../src/loader/fragment';
import type { Level } from '../../src/types/level';
import type { TrackSet } from '../../src/types/track';
import type { MediaPlaylist } from '../../src/types/media-playlist';
import type { LevelDetails } from '../../src/loader/level-details';
import {
FragChangedData,
FragLoadedData,
FragParsedData,
} from '../../src/types/events';
declare global {
interface Window {
Hls: any;
hls: any;
chart: any;
}
}
const X_AXIS_SECONDS = 'x-axis-seconds';
interface ChartScale {
width: number;
height: number;
min: number;
max: number;
options: any;
determineDataLimits: () => void;
buildTicks: () => void;
getLabelForIndex: (index: number, datasetIndex: number) => string;
getPixelForTick: (index: number) => number;
getPixelForValue: (
value: number,
index?: number,
datasetIndex?: number
) => number;
getValueForPixel: (pixel: number) => number;
}
export class TimelineChart {
private readonly chart: Chart;
private rafDebounceRequestId: number = -1;
private imageDataBuffer: ImageData | null = null;
private media: HTMLMediaElement | null = null;
private tracksChangeHandler?: (e) => void;
private cuesChangeHandler?: (e) => void;
private hidden: boolean = true;
private zoom100: number = 60;
constructor(canvas: HTMLCanvasElement, chartJsOptions?: any) {
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error(
`Could not get CanvasRenderingContext2D from canvas: ${canvas}`
);
}
const chart =
(this.chart =
self.chart =
new Chart(ctx, {
type: 'horizontalBar',
data: {
labels: [],
datasets: [],
},
options: Object.assign(getChartOptions(), chartJsOptions),
plugins: [
{
afterRender: (chart) => {
this.imageDataBuffer = null;
this.drawCurrentTime();
},
},
],
}));
applyChartInstanceOverrides(chart);
canvas.ondblclick = (event: MouseEvent) => {
const chart = this.chart;
const chartArea: { left; top; right; bottom } = chart.chartArea;
const element = chart.getElementAtEvent(event);
const pos = Chart.helpers.getRelativePosition(event, chart);
const scale = this.chartScales[X_AXIS_SECONDS];
// zoom in when double clicking near elements in chart area
if (element.length || pos.x > chartArea.left) {
const amount = event.getModifierState('Shift') ? -1.0 : 0.5;
this.zoom(scale, pos, amount);
} else {
scale.options.ticks.min = 0;
scale.options.ticks.max = this.zoom100;
}
this.update();
};
canvas.onwheel = (event: WheelEvent) => {
if (event.deltaMode) {
// exit if wheel is in page or line scrolling mode
return;
}
const chart = this.chart;
const chartArea: { left; top; right; bottom } = chart.chartArea;
const pos = Chart.helpers.getRelativePosition(event, chart);
// zoom when scrolling over chart elements
if (pos.x > chartArea.left - 11) {
const scale = this.chartScales[X_AXIS_SECONDS];
if (event.deltaY) {
const direction = -event.deltaY / Math.abs(event.deltaY);
const normal = Math.min(333, Math.abs(event.deltaY)) / 1000;
const ease = 1 - (1 - normal) * (1 - normal);
this.zoom(scale, pos, ease * direction);
} else if (event.deltaX) {
this.pan(scale, event.deltaX / 10, scale.min, scale.max);
}
event.preventDefault();
}
};
let moved = false;
let gestureScale = 1;
canvas.onpointerdown = (downEvent: PointerEvent) => {
if (!downEvent.isPrimary || gestureScale !== 1) {
return;
}
const chart = this.chart;
const chartArea: { left; top; right; bottom } = chart.chartArea;
const pos = Chart.helpers.getRelativePosition(downEvent, chart);
// pan when dragging over chart elements
if (pos.x > chartArea.left) {
const scale = this.chartScales[X_AXIS_SECONDS];
const startX = downEvent.clientX;
const { min, max } = scale;
const xToVal = (max - min) / scale.width;
moved = false;
canvas.setPointerCapture(downEvent.pointerId);
canvas.onpointermove = (moveEvent: PointerEvent) => {
if (!downEvent.isPrimary || gestureScale !== 1) {
return;
}
const movedX = startX - moveEvent.clientX;
const movedValue = movedX * xToVal;
moved = moved || Math.abs(movedX) > 8;
this.pan(scale, movedValue, min, max);
};
}
};
canvas.onpointerup = canvas.onpointercancel = (upEvent: PointerEvent) => {
if (canvas.onpointermove) {
canvas.onpointermove = null;
canvas.releasePointerCapture(upEvent.pointerId);
}
if (!moved && upEvent.isPrimary) {
this.click(upEvent);
}
};
// Gesture events are for iOS and easier to implement than pinch-zoom with multiple pointers for all browsers
// @ts-ignore
canvas.ongesturestart = (event) => {
gestureScale = 1;
event.preventDefault();
};
// @ts-ignore
canvas.ongestureend = (event) => {
gestureScale = 1;
};
// @ts-ignore
canvas.ongesturechange = (event) => {
const chart = this.chart;
const chartArea: { left; top; right; bottom } = chart.chartArea;
const pos = Chart.helpers.getRelativePosition(event, chart);
// zoom when scrolling over chart elements
if (pos.x > chartArea.left) {
const scale = this.chartScales[X_AXIS_SECONDS];
const amount = event.scale - gestureScale;
this.zoom(scale, pos, amount);
gestureScale = event.scale;
}
};
}
private click(event: MouseEvent) {
// Log object on click and seek to position
const chart = this.chart;
const element = chart.getElementAtEvent(event);
if (element.length && chart.data.datasets) {
const dataset = chart.data.datasets[(element[0] as any)._datasetIndex];
const obj = dataset.data![(element[0] as any)._index];
// eslint-disable-next-line no-console
console.log(obj);
if (self.hls?.media) {
const scale = this.chartScales[X_AXIS_SECONDS];
const pos = Chart.helpers.getRelativePosition(event, chart);
self.hls.media.currentTime = scale.getValueForPixel(pos.x);
}
}
}
private pan(scale: ChartScale, amount: number, min: number, max: number) {
if (amount === 0) {
return;
}
let pan = amount;
if (amount > 0) {
pan = Math.min(this.zoom100 + 10 - max, amount);
} else {
pan = Math.max(-10 - min, amount);
}
scale.options.ticks.min = min + pan;
scale.options.ticks.max = max + pan;
this.updateOnRepaint();
}
private zoom(scale: ChartScale, pos: any, amount: number) {
const range = scale.max - scale.min;
const diff = range * amount;
const minPercent = (scale.getValueForPixel(pos.x) - scale.min) / range;
const maxPercent = 1 - minPercent;
const minDelta = diff * minPercent;
const maxDelta = diff * maxPercent;
scale.options.ticks.min = Math.max(-10, scale.min + minDelta);
scale.options.ticks.max = Math.min(this.zoom100 + 10, scale.max - maxDelta);
this.updateOnRepaint();
}
get chartScales(): { 'x-axis-seconds': ChartScale } {
return (this.chart as any).scales;
}
reset() {
const scale = this.chartScales[X_AXIS_SECONDS];
scale.options.ticks.min = 0;
scale.options.ticks.max = 60;
const { labels, datasets } = this.chart.data;
if (labels && datasets) {
labels.length = 0;
datasets.length = 0;
this.resize(datasets);
}
}
update() {
if (this.hidden || !this.chart.ctx?.canvas.width) {
return;
}
self.cancelAnimationFrame(this.rafDebounceRequestId);
this.chart.update({
duration: 0,
lazy: true,
});
}
updateOnRepaint() {
if (this.hidden) {
return;
}
self.cancelAnimationFrame(this.rafDebounceRequestId);
this.rafDebounceRequestId = self.requestAnimationFrame(() => this.update());
}
resize(datasets?) {
if (this.hidden) {
return;
}
if (datasets?.length) {
const scale = this.chartScales[X_AXIS_SECONDS];
const { top } = this.chart.chartArea;
const height =
top +
datasets.reduce((val, dataset) => val + dataset.barThickness, 0) +
scale.height +
5;
const container = this.chart.canvas?.parentElement;
if (container) {
container.style.height = `${height}px`;
}
}
self.cancelAnimationFrame(this.rafDebounceRequestId);
this.rafDebounceRequestId = self.requestAnimationFrame(() => {
this.chart.resize();
});
}
show() {
this.hidden = false;
}
hide() {
this.hidden = true;
}
updateLevels(levels: Level[], levelSwitched) {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
}
const { loadLevel, nextLoadLevel, nextAutoLevel } = self.hls;
// eslint-disable-next-line no-undefined
const currentLevel =
levelSwitched !== undefined ? levelSwitched : self.hls.currentLevel;
levels.forEach((level, i) => {
const index = level.id || i;
labels.push(getLevelName(level, index));
let borderColor: string | null = null;
if (currentLevel === i) {
borderColor = 'rgba(32, 32, 240, 1.0)';
} else if (loadLevel === i) {
borderColor = 'rgba(255, 128, 0, 1.0)';
} else if (nextLoadLevel === i) {
borderColor = 'rgba(200, 200, 64, 1.0)';
} else if (nextAutoLevel === i) {
borderColor = 'rgba(160, 0, 160, 1.0)';
}
datasets.push(
datasetWithDefaults({
url: Array.isArray(level.url)
? level.url[level.urlId || 0]
: level.url,
trackType: 'level',
borderColor,
level: index,
})
);
if (level.details) {
this.updateLevelOrTrack(level.details);
}
});
this.resize(datasets);
}
updateAudioTracks(audioTracks: MediaPlaylist[]) {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
}
const { audioTrack } = self.hls;
audioTracks.forEach((track: MediaPlaylist, i) => {
labels.push(getAudioTrackName(track, i));
datasets.push(
datasetWithDefaults({
url: track.url,
trackType: 'audioTrack',
borderColor: audioTrack === i ? 'rgba(32, 32, 240, 1.0)' : null,
audioTrack: i,
})
);
if (track.details) {
this.updateLevelOrTrack(track.details);
}
});
this.resize(datasets);
}
updateSubtitleTracks(subtitles: MediaPlaylist[]) {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
}
const { subtitleTrack } = self.hls;
subtitles.forEach((track, i) => {
labels.push(getSubtitlesName(track, i));
datasets.push(
datasetWithDefaults({
url: track.url,
trackType: 'subtitleTrack',
borderColor: subtitleTrack === i ? 'rgba(32, 32, 240, 1.0)' : null,
subtitleTrack: i,
})
);
if (track.details) {
this.updateLevelOrTrack(track.details);
}
});
this.resize(datasets);
}
removeType(
trackType: 'level' | 'audioTrack' | 'subtitleTrack' | 'textTrack'
) {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
}
let i = datasets.length;
while (i--) {
if ((datasets[i] as any).trackType === trackType) {
datasets.splice(i, 1);
labels.splice(i, 1);
}
}
}
updateLevelOrTrack(details: LevelDetails) {
const { targetduration, totalduration, url } = details;
const { datasets } = this.chart.data;
let levelDataSet = arrayFind(
datasets,
(dataset) =>
stripDeliveryDirectives(url) ===
stripDeliveryDirectives(dataset.url || '')
);
if (!levelDataSet) {
levelDataSet = arrayFind(
datasets,
(dataset) => details.fragments[0]?.level === dataset.level
);
}
if (!levelDataSet) {
return;
}
const data = levelDataSet.data;
data.length = 0;
if (details.fragments) {
details.fragments.forEach((fragment) => {
// TODO: keep track of initial playlist start and duration so that we can show drift and pts offset
// (Make that a feature of hls.js v1.0.0 fragments)
const chartFragment = Object.assign(
{
dataType: 'fragment',
},
fragment,
// Remove loader references for GC
{ loader: null }
);
data.push(chartFragment);
});
}
if (details.partList) {
details.partList.forEach((part) => {
const chartPart = Object.assign(
{
dataType: 'part',
start: part.fragment.start + part.fragOffset,
},
part,
{
fragment: Object.assign({}, part.fragment, { loader: null }),
}
);
data.push(chartPart);
});
if (details.fragmentHint) {
const chartFragment = Object.assign(
{
dataType: 'fragmentHint',
},
details.fragmentHint,
// Remove loader references for GC
{ loader: null }
);
data.push(chartFragment);
}
}
const start = getPlaylistStart(details);
this.maxZoom = this.zoom100 = Math.max(
start + totalduration + targetduration * 3,
this.zoom100
);
this.updateOnRepaint();
}
// @ts-ignore
get minZoom(): number {
const scale = this.chartScales[X_AXIS_SECONDS];
if (scale) {
return scale.options.ticks.min;
}
return 1;
}
// @ts-ignore
get maxZoom(): number {
const scale = this.chartScales[X_AXIS_SECONDS];
if (scale) {
return scale.options.ticks.max;
}
return this.zoom100;
}
// @ts-ignore
set maxZoom(x: number) {
const currentZoom = this.maxZoom;
const newZoom = Math.max(x, currentZoom);
if (currentZoom === 60 && newZoom !== currentZoom) {
const scale = this.chartScales[X_AXIS_SECONDS];
scale.options.ticks.max = newZoom;
}
}
updateFragment(data: FragLoadedData | FragParsedData | FragChangedData) {
const { datasets } = this.chart.data;
const frag: Fragment = data.frag;
let levelDataSet = arrayFind(
datasets,
(dataset) => frag.baseurl === dataset.url
);
if (!levelDataSet) {
levelDataSet = arrayFind(
datasets,
(dataset) => frag.level === dataset.level
);
}
if (!levelDataSet) {
return;
}
// eslint-disable-next-line no-restricted-properties
const fragData = arrayFind(
levelDataSet.data,
(fragData) => fragData.relurl === frag.relurl && fragData.sn === frag.sn
);
if (fragData && fragData !== frag) {
Object.assign(fragData, frag);
}
this.updateOnRepaint();
}
updateSourceBuffers(tracks: TrackSet, media: HTMLMediaElement) {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
}
const trackTypes = Object.keys(tracks).sort((type) =>
type === 'video' ? 1 : -1
);
const mediaBufferData = [];
this.removeSourceBuffers();
this.media = media;
trackTypes.forEach((type) => {
const track = tracks[type];
const data = [];
const sourceBuffer = track.buffer;
const backgroundColor = {
video: 'rgba(0, 0, 255, 0.2)',
audio: 'rgba(128, 128, 0, 0.2)',
audiovideo: 'rgba(128, 128, 255, 0.2)',
}[type];
labels.unshift(`${type} buffer (${track.id})`);
datasets.unshift(
datasetWithDefaults({
data,
categoryPercentage: 0.5,
backgroundColor,
sourceBuffer,
})
);
sourceBuffer.addEventListener('update', () => {
try {
replaceTimeRangeTuples(sourceBuffer.buffered, data);
} catch (error) {
// eslint-disable-next-line no-console
console.warn(error);
return;
}
replaceTimeRangeTuples(media.buffered, mediaBufferData);
this.update();
});
});
if (trackTypes.length === 0) {
media.onprogress = () => {
replaceTimeRangeTuples(media.buffered, mediaBufferData);
this.update();
};
}
labels.unshift('media buffer');
datasets.unshift(
datasetWithDefaults({
data: mediaBufferData,
categoryPercentage: 0.5,
backgroundColor: 'rgba(0, 255, 0, 0.2)',
media,
})
);
media.ontimeupdate = () => this.drawCurrentTime();
// TextTrackList
const { textTracks } = media;
this.tracksChangeHandler =
this.tracksChangeHandler || ((e) => this.setTextTracks(e.currentTarget));
textTracks.removeEventListener('addtrack', this.tracksChangeHandler);
textTracks.removeEventListener('removetrack', this.tracksChangeHandler);
textTracks.removeEventListener('change', this.tracksChangeHandler);
textTracks.addEventListener('addtrack', this.tracksChangeHandler);
textTracks.addEventListener('removetrack', this.tracksChangeHandler);
textTracks.addEventListener('change', this.tracksChangeHandler);
this.setTextTracks(textTracks);
this.resize(datasets);
}
removeSourceBuffers() {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
}
let i = datasets.length;
while (i--) {
if ((labels[0] || '').toString().indexOf('buffer') > -1) {
datasets.splice(i, 1);
labels.splice(i, 1);
}
}
}
setTextTracks(textTracks) {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
}
this.removeType('textTrack');
[].forEach.call(textTracks, (textTrack, i) => {
// Uncomment to disable rending of subtitle/caption cues in the timeline
// if (textTrack.kind === 'subtitles' || textTrack.kind === 'captions') {
// return;
// }
const data = [];
labels.push(
`${textTrack.name || textTrack.label} ${textTrack.kind} (${
textTrack.mode
})`
);
datasets.push(
datasetWithDefaults({
data,
categoryPercentage: 0.5,
url: '',
trackType: 'textTrack',
borderColor:
(textTrack.mode !== 'hidden') === i
? 'rgba(32, 32, 240, 1.0)'
: null,
textTrack: i,
})
);
this.cuesChangeHandler =
this.cuesChangeHandler ||
((e) => this.updateTextTrackCues(e.currentTarget));
textTrack._data = data;
textTrack.removeEventListener('cuechange', this.cuesChangeHandler);
textTrack.addEventListener('cuechange', this.cuesChangeHandler);
this.updateTextTrackCues(textTrack);
});
this.resize(datasets);
}
updateTextTrackCues(textTrack) {
const data = textTrack._data;
if (!data) {
return;
}
const { activeCues, cues } = textTrack;
data.length = 0;
if (!cues) {
return;
}
const length = cues.length;
let activeLength = 0;
let activeMin = Infinity;
let activeMax = 0;
if (activeCues) {
activeLength = activeCues.length;
for (let i = 0; i < activeLength; i++) {
let cue = activeCues[i];
if (!cue && activeCues.item) {
cue = activeCues.item(i);
}
if (cue) {
activeMin = Math.min(activeMin, cue.startTime);
activeMax = cue.endTime
? Math.max(activeMax, cue.endTime)
: activeMax;
} else {
activeLength--;
}
}
}
for (let i = 0; i < length; i++) {
let cue = cues[i];
if (!cue && cues.item) {
cue = cues.item(i);
}
if (!cue) {
continue;
}
const start = cue.startTime;
const end = cue.endTime;
const content = getCueLabel(cue);
let active = false;
if (activeLength && end >= activeMin && start <= activeMax) {
active = [].some.call(activeCues, (activeCue) =>
cuesMatch(activeCue, cue)
);
}
data.push({
start,
end,
content,
active,
dataType: 'cue',
});
}
this.updateOnRepaint();
}
drawCurrentTime() {
const chart = this.chart;
if (self.hls?.media && chart.data.datasets!.length) {
const currentTime = self.hls.media.currentTime;
const scale = this.chartScales[X_AXIS_SECONDS];
const ctx = chart.ctx;
if (this.hidden || !ctx || !ctx.canvas.width) {
return;
}
const chartArea: { left; top; right; bottom } = chart.chartArea;
const x = scale.getPixelForValue(currentTime);
ctx.restore();
ctx.save();
this.drawLineX(ctx, x, chartArea);
if (x > chartArea.left && x < chartArea.right) {
ctx.fillStyle = this.getCurrentTimeColor(self.hls.media);
const y = chartArea.top + chart.data.datasets![0].barThickness + 1;
ctx.fillText(hhmmss(currentTime, 5), x + 2, y, 100);
}
ctx.restore();
}
}
getCurrentTimeColor(video: HTMLMediaElement): string {
if (!video.readyState || video.ended) {
return 'rgba(0, 0, 0, 0.9)';
}
if (video.seeking || video.readyState < 3) {
return 'rgba(255, 128, 0, 0.9)';
}
if (video.paused) {
return 'rgba(128, 0, 255, 0.9)';
}
return 'rgba(0, 0, 255, 0.9)';
}
drawLineX(ctx, x: number, chartArea) {
if (!this.imageDataBuffer) {
const devicePixelRatio = self.devicePixelRatio || 1;
this.imageDataBuffer = ctx.getImageData(
0,
0,
chartArea.right * devicePixelRatio,
chartArea.bottom * devicePixelRatio
);
} else {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, chartArea.right, chartArea.bottom);
ctx.putImageData(this.imageDataBuffer, 0, 0);
}
if (x > chartArea.left && x < chartArea.right) {
ctx.lineWidth = 1;
ctx.strokeStyle = this.getCurrentTimeColor(self.hls.media); // alpha '0.5'
ctx.beginPath();
ctx.moveTo(x, chartArea.top);
ctx.lineTo(x, chartArea.bottom);
ctx.stroke();
}
}
}
function stripDeliveryDirectives(url: string): string {
if (url === '') {
return url;
}
try {
const webUrl: URL = new self.URL(url);
webUrl.searchParams.delete('_HLS_msn');
webUrl.searchParams.delete('_HLS_part');
webUrl.searchParams.delete('_HLS_skip');
webUrl.searchParams.sort();
return webUrl.href;
} catch (e) {
return url.replace(/[?&]_HLS_(?:msn|part|skip)=[^?&]+/g, '');
}
}
function datasetWithDefaults(options) {
return Object.assign(
{
data: [],
xAxisID: X_AXIS_SECONDS,
barThickness: 35,
categoryPercentage: 1,
},
options
);
}
function getPlaylistStart(details: LevelDetails): number {
return details.fragments?.length ? details.fragments[0].start : 0;
}
function getLevelName(level: Level, index: number) {
let label = '(main playlist)';
if (level.attrs?.BANDWIDTH) {
label = `${getMainLevelAttribute(level)}@${level.attrs.BANDWIDTH}`;
if (level.name) {
label = `${label} (${level.name})`;
}
} else if (level.name) {
label = level.name;
}
return `${label} L-${index}`;
}
function getMainLevelAttribute(level: Level) {
return level.attrs.RESOLUTION || level.attrs.CODECS || level.attrs.AUDIO;
}
function getAudioTrackName(track: MediaPlaylist, index: number) {
const label = track.lang ? `${track.name}/${track.lang}` : track.name;
return `${label} (${track.groupId || track.attrs['GROUP-ID']}) A-${index}`;
}
function getSubtitlesName(track: MediaPlaylist, index: number) {
const label = track.lang ? `${track.name}/${track.lang}` : track.name;
return `${label} (${track.groupId || track.attrs['GROUP-ID']}) S-${index}`;
}
function replaceTimeRangeTuples(timeRanges, data) {
data.length = 0;
const { length } = timeRanges;
for (let i = 0; i < length; i++) {
data.push([timeRanges.start(i), timeRanges.end(i)]);
}
}
function cuesMatch(cue1, cue2) {
return (
cue1.startTime === cue2.startTime &&
cue1.endTime === cue2.endTime &&
cue1.text === cue2.text &&
cue1.data === cue2.data &&
JSON.stringify(cue1.value) === JSON.stringify(cue2.value)
);
}
function getCueLabel(cue) {
if (cue.text) {
return cue.text;
}
const result = parseDataCue(cue);
return JSON.stringify(result);
}
function parseDataCue(cue) {
const data = {};
const { value } = cue;
if (value) {
if (value.info) {
let collection = data[value.key];
if (collection !== Object(collection)) {
collection = {};
data[value.key] = collection;
}
collection[value.info] = value.data;
} else {
data[value.key] = value.data;
}
}
return data;
}
function getChartOptions() {
return {
animation: {
duration: 0,
},
elements: {
rectangle: {
borderWidth: 1,
borderColor: 'rgba(20, 20, 20, 1)',
},
},
events: ['click', 'touchstart'],
hover: {
mode: null,
animationDuration: 0,
},
legend: {
display: false,
},
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
scales: {
// TODO: additional xAxes for PTS and PDT
xAxes: [
{
id: X_AXIS_SECONDS,
ticks: {
beginAtZero: true,
sampleSize: 0,
maxRotation: 0,
callback: (tickValue, i, ticks) => {
if (i === 0 || i === ticks.length - 1) {
return tickValue ? '' : '0';
} else {
return hhmmss(tickValue, 2);
}
},
},
},
],
yAxes: [
{
gridLines: {
display: false,
},
},
],
},
tooltips: {
enabled: false,
},
};
}
function arrayFind(array, predicate) {
const len = array.length >>> 0;
if (typeof predicate !== 'function') {
throw TypeError('predicate must be a function');
}
const thisArg = arguments[2];
let k = 0;
while (k < len) {
const kValue = array[k];
if (predicate.call(thisArg, kValue, k, array)) {
return kValue;
}
k++;
}
// eslint-disable-next-line no-undefined
return undefined;
}
+30
View File
@@ -0,0 +1,30 @@
export function sortObject(obj) {
if (typeof obj !== 'object') {
return obj;
}
let temp = {};
let keys = [];
for (let key in obj) {
keys.push(key);
}
keys.sort();
for (let index in keys) {
temp[keys[index]] = sortObject(obj[keys[index]]);
}
return temp;
}
export function copyTextToClipboard(text) {
let textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
try {
let successful = document.execCommand('copy');
let msg = successful ? 'successful' : 'unsuccessful';
console.log('Copying text command was ' + msg);
} catch (err) {
console.log('Oops, unable to copy');
}
document.body.removeChild(textArea);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+675
View File
@@ -0,0 +1,675 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>hls.js demo</title>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"
/>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap-theme.min.css"
/>
<link rel="stylesheet" href="style.css" />
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.13.1/ace.js"></script>
</head>
<body>
<div class="header-container">
<header class="wrapper clearfix">
<h1>
<a target="_blank" href="https://github.com/video-dev/hls.js">
<img
src="https://cloud.githubusercontent.com/assets/616833/19739063/e10be95a-9bb9-11e6-8100-2896f8500138.png"
/>
</a>
</h1>
<h2 class="title">light demo</h2>
<h3>
<a href="../api-docs">API docs | usage guide</a>
</h3>
</header>
</div>
<div class="main-container">
<header>
<p>
Test your HLS streams in all supported browsers
(Chrome/Firefox/IE11/Edge/Safari).
</p>
<p>Advanced controls are available at the bottom of this page.</p>
<p>
<b
>This page uses hls.light.js. It does not support EME, subs or
alt-audio. Looking for the <i>complete hls.js</i> demo? Go
<a href=".">here</a>.</b
><br />
</p>
</header>
<div id="controls">
<div class="demo-controls-wrapper">
<select id="streamSelect" class="innerControls">
<option value="" selected>
Select a test-stream from drop-down menu. Or enter custom URL
below
</option>
</select>
<input id="streamURL" class="innerControls" type="text" value="" />
<label
class="innerControls"
title="Uncheck this to disable loading of streams selected from the drop-down above."
>
Enable streaming:
<input id="enableStreaming" type="checkbox" checked />
</label>
<label
class="innerControls"
title="When a media error occurs, attempt to recover playback by calling `hls.recoverMediaError()`."
>
Auto-recover media-errors:
<input id="autoRecoverError" type="checkbox" checked />
</label>
<label
class="innerControls"
title="Stop loading and playback if playback under-buffer stalls. This can help debug stall errors."
>
Stop on first stall:
<input id="stopOnStall" type="checkbox" unchecked />
</label>
<label class="innerControls">
Dump transmuxed fMP4 data:
<input id="dumpfMP4" type="checkbox" unchecked />
</label>
<label class="innerControls">
Metrics history (max limit, -1 is unlimited):
<input id="limitMetrics" style="width: 8em" type="number" />
</label>
<label class="innerControls">
HTML video element width:
<select id="videoSize" style="float: right">
<option value="240px">240px</option>
<option value="426px">426px</option>
<option value="640px">640px</option>
<option value="720px">720px</option>
<option value="854px">854px</option>
<option value="1280px">1280px</option>
<option value="1920px">1920px</option>
<option value="80%">Responsive (80%)</option>
<option value="100%">Responsive (100%)</option>
</select>
</label>
<label class="innerControls">
Current player size:
<span id="currentSize"></span>
</label>
<label class="innerControls">
Current video resolution:
<span id="currentResolution"></span>
</label>
<label class="innerControls permalink">
Permalink:&nbsp;&nbsp;<span id="StreamPermalink"></span>
</label>
</div>
<div class="config-editor-wrapper">
<div class="config-editor-container">
<div id="config-editor">Loading...</div>
</div>
<div class="config-editor-commands">
<label for="config-persistence">
Persist
<input
name="config-persistence"
id="config-persistence"
type="checkbox"
/>
</label>
<button name="config-apply" onclick="applyConfigEditorValue()">
Apply
</button>
</div>
</div>
</div>
<video
id="video"
controls
autoplay
class="videoCentered"
style="width: 80%"
></video>
<br />
<canvas
id="bufferedCanvas"
width="720"
height="15"
class="videoCentered"
onclick="onClickBufferedRange(event);"
style="height: fit-content"
></canvas>
<br />
<br />
<label class="center">Status:</label>
<pre id="statusOut" class="center" style="white-space: pre-wrap"></pre>
<label class="center">Error:</label>
<pre id="errorOut" class="center" style="white-space: pre-wrap"></pre>
<div
class="center"
style="text-align: center; display: none"
id="toggleButtons"
>
<button
type="button"
class="btn btn-sm demo-tab-btn"
data-tab="playbackControlTab"
onclick="toggleTabClick(this);"
>
Playback
</button>
<button
type="button"
class="btn btn-sm demo-tab-btn"
data-tab="timelineTab"
onclick="toggleTabClick(this);"
>
Timeline
</button>
<button
type="button"
class="btn btn-sm demo-tab-btn"
data-tab="qualityLevelControlTab"
onclick="toggleTabClick(this);"
>
Quality-levels
</button>
<button
type="button"
class="btn btn-sm demo-tab-btn"
data-tab="audioTrackControlTab"
onclick="toggleTabClick(this);"
>
Audio-tracks
</button>
<button
type="button"
class="btn btn-sm demo-tab-btn"
data-tab="metricsDisplayTab"
onclick="toggleTabClick(this); showMetrics();"
>
Real-time metrics
</button>
<button
type="button"
class="btn btn-sm demo-tab-btn"
data-tab="statsDisplayTab"
onclick="toggleTabClick(this);"
>
Buffer &amp; Statistics
</button>
</div>
<div
class="center demo-tab"
id="playbackControlTab"
style="display: none"
>
<br />
<center>
<p>
<span>
<button
type="button"
class="btn btn-sm btn-info"
title="video.play()"
onclick="$('#video')[0].play()"
>
Play
</button>
<button
type="button"
class="btn btn-sm btn-info"
title="video.pause()"
onclick="$('#video')[0].pause()"
>
Pause
</button>
</span>
<span>
<button
type="button"
class="btn btn-sm btn-info"
title="video.playbackRate = text input"
onclick="$('#video')[0].defaultPlaybackRate=$('#video')[0].playbackRate=$('#playback_rate').val();"
>
Playback rate
</button>
<input
type="number"
value="1"
id="playback_rate"
size="8"
style="width: 3em"
onkeydown="if(window.event.keyCode=='13'){$('#video')[0].defaultPlaybackRate=$('#video')[0].playbackRate=$('#playback_rate').val();}"
/>
</span>
<span>
<button
type="button"
class="btn btn-sm btn-info"
title="video.currentTime -= 10"
onclick="$('#video')[0].currentTime-=10"
>
- 10 s
</button>
<button
type="button"
class="btn btn-sm btn-info"
title="video.currentTime += 10"
onclick="$('#video')[0].currentTime+=10"
>
+ 10 s
</button>
</span>
<span>
<button
type="button"
class="btn btn-sm btn-info"
title="video.currentTime = text input"
onclick="$('#video')[0].currentTime=$('#seek_pos').val();"
>
Seek to
</button>
<input
type="number"
id="seek_pos"
size="8"
style="width: 7em"
onkeydown="if(window.event.keyCode=='13'){$('#video')[0].currentTime=$('#seek_pos').val();}"
/>
</span>
</p>
<p>
<span>
<button
type="button"
class="btn btn-xs btn-warning"
title="hls.startLoad()"
onclick="hls.startLoad()"
>
Start loading
</button>
<button
type="button"
class="btn btn-xs btn-warning"
title="hls.stopLoad()"
onclick="hls.stopLoad()"
>
Stop loading
</button>
</span>
<span>
<button
type="button"
class="btn btn-xs btn-warning"
title="hls.attachMedia(video)"
onclick="hls.attachMedia($('#video')[0])"
>
Attach media
</button>
<button
type="button"
class="btn btn-xs btn-warning"
title="hls.detachMedia()"
onclick="hls.detachMedia()"
>
Detach media
</button>
</span>
</p>
<p>
<span>
<button
type="button"
class="btn btn-xs btn-warning"
title="hls.recoverMediaError()"
onclick="hls.recoverMediaError()"
>
Recover media-error
</button>
<button
type="button"
class="btn btn-xs btn-warning"
title="hls.swapAudioCodec()"
onclick="hls.swapAudioCodec()"
>
Swap audio codec
</button>
</span>
</p>
<p>
<span>
<button
type="button"
class="btn btn-xs btn-default"
onclick="$('#streamSelect')[0].selectedIndex++;$('#streamSelect').change()"
>
Next video
</button>
<button
type="button"
class="btn btn-xs btn-default btn-dump"
title="Save dumped audio mp4 appends"
onclick="createfMP4('audio');"
>
Create audio-fmp4
</button>
<button
type="button"
class="btn btn-xs btn-default btn-dump"
title="Save dumped video mp4 appends"
onclick="createfMP4('video')"
>
Create video-fmp4
</button>
</span>
</p>
</center>
</div>
<div
class="center demo-tab demo-timeline-chart-container"
id="timelineTab"
style="display: none"
>
<canvas id="timeline-chart"></canvas>
</div>
<div
class="center demo-tab"
id="qualityLevelControlTab"
style="display: none"
>
<center>
<table>
<tr>
<td>
<p>Currently played level:</p>
</td>
<td>
<div id="currentLevelControl" style="display: inline"></div>
</td>
</tr>
<tr>
<td>
<p>Next level loaded:</p>
</td>
<td>
<div id="nextLevelControl" style="display: inline"></div>
</td>
</tr>
<tr>
<td>
<p>Currently loaded level:</p>
</td>
<td>
<div id="loadLevelControl" style="display: inline"></div>
</td>
</tr>
<tr>
<td>
<p>Cap-limit level (maximum):</p>
</td>
<td>
<div id="levelCappingControl" style="display: inline"></div>
</td>
</tr>
</table>
</center>
</div>
<div
class="center demo-tab"
id="audioTrackControlTab"
style="display: none"
>
<table>
<tr>
<td>Current audio-track:</td>
<td><div id="audioTrackControl" style="display: inline"></div></td>
</tr>
<tr>
<td>Language / Name:</td>
<td>
<div id="audioTrackLabel" style="display: inline">
None selected
</div>
</td>
</tr>
</table>
</div>
<div class="center demo-tab" id="metricsDisplayTab" style="display: none">
<br />
<div id="metricsButton">
<button
type="button"
class="btn btn-xs btn-info"
onclick="$('#metricsButtonWindow').toggle();$('#metricsButtonFixed').toggle();windowSliding=!windowSliding; refreshCanvas()"
>
toggle sliding/fixed window</button
><br />
<div id="metricsButtonWindow">
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(0)"
>
window ALL
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(2000)"
>
2s
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(5000)"
>
5s
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(10000)"
>
10s
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(20000)"
>
20s
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(30000)"
>
30s
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(60000)"
>
60s
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(120000)"
>
120s</button
><br />
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeZoomIn()"
>
Window Zoom In
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeZoomOut()"
>
Window Zoom Out</button
><br />
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSlideLeft()"
>
<<< Window Slide
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSlideRight()"
>
Window Slide >>></button
><br />
</div>
<div id="metricsButtonFixed">
<button
type="button"
class="btn btn-xs btn-info"
onclick="windowStart=$('#windowStart').val()"
>
fixed window start(ms)
</button>
<input
type="text"
id="windowStart"
defaultValue="0"
size="8"
onkeydown="if(window.event.keyCode=='13'){windowStart=$('#windowStart').val();}"
/>
<button
type="button"
class="btn btn-xs btn-info"
onclick="windowEnd=$('#windowEnd').val()"
>
fixed window end(ms)
</button>
<input
type="text"
id="windowEnd"
defaultValue="10000"
size="8"
onkeydown="if(window.event.keyCode=='13'){windowEnd=$('#windowEnd').val();}"
/><br />
</div>
<button
type="button"
class="btn btn-xs btn-success"
onclick="goToMetrics()"
style="font-size: 18px"
>
metrics link
</button>
<button
type="button"
class="btn btn-xs btn-success"
onclick="goToMetricsPermaLink()"
style="font-size: 18px"
>
metrics permalink
</button>
<button
type="button"
class="btn btn-xs btn-success"
onclick="copyMetricsToClipBoard()"
style="font-size: 18px"
>
copy metrics to clipboard
</button>
<canvas
id="bufferTimerange_c"
width="640"
height="100"
style="border: 1px solid #000000"
onmousedown="timeRangeCanvasonMouseDown(event)"
onmousemove="timeRangeCanvasonMouseMove(event)"
onmouseup="timeRangeCanvasonMouseUp(event)"
onmouseout="timeRangeCanvasonMouseOut(event);"
></canvas>
<canvas
id="bitrateTimerange_c"
width="640"
height="100"
style="border: 1px solid #000000"
></canvas>
<canvas
id="bufferWindow_c"
width="640"
height="100"
style="border: 1px solid #000000"
onmousemove="windowCanvasonMouseMove(event);"
></canvas>
<canvas
id="videoEvent_c"
width="640"
height="15"
style="border: 1px solid #000000"
></canvas>
<canvas
id="loadEvent_c"
width="640"
height="15"
style="border: 1px solid #000000"
></canvas
><br />
</div>
</div>
<div class="center demo-tab" id="statsDisplayTab" style="display: none">
<br />
<label>Buffer state:</label>
<pre id="bufferedOut"></pre>
<label>General stats:</label>
<pre id="statisticsOut"></pre>
</div>
</div>
<footer><br /><br /><br /><br /><br /><br /></footer>
<!-- Demo page required libs -->
<script src="canvas.js"></script>
<script src="metrics.js"></script>
<!-- demo build -->
<script src="hls.light.js"></script>
<script src="hls-demo.js"></script>
</body>
</html>
+673
View File
@@ -0,0 +1,673 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>hls.js demo</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.4/dist/css/bootstrap.min.css"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.4/dist/css/bootstrap-theme.min.css"
/>
<link rel="stylesheet" href="style.css" />
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.4/dist/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.13.1/ace.js"></script>
</head>
<body>
<div class="header-container">
<header class="wrapper clearfix">
<h1>
<a target="_blank" href="https://github.com/video-dev/hls.js">
<img
src="https://cloud.githubusercontent.com/assets/616833/19739063/e10be95a-9bb9-11e6-8100-2896f8500138.png"
/>
</a>
</h1>
<h2 class="title">demo</h2>
<h3>
<a href="../api-docs">API docs | usage guide</a>
</h3>
</header>
</div>
<div class="main-container">
<header>
<p>
Test your HLS streams in all supported browsers
(Chrome/Firefox/IE11/Edge/Safari).
</p>
<p>Advanced controls are available at the bottom of this page.</p>
<p>
<b
>Looking for a more <i>basic</i> usage example? Go
<a href="basic-usage.html">here</a>.</b
><br />
</p>
</header>
<div id="controls">
<div class="demo-controls-wrapper">
<select id="streamSelect" class="innerControls">
<option value="" selected>
选择或输入地址
</option>
</select>
<input id="streamURL" class="innerControls" type="text" value="" />
<label
class="innerControls"
title="Uncheck this to disable loading of streams selected from the drop-down above."
>
Enable streaming:
<input id="enableStreaming" type="checkbox" checked />
</label>
<label
class="innerControls"
title="When a media error occurs, attempt to recover playback by calling `hls.recoverMediaError()`."
>
Auto-recover media-errors:
<input id="autoRecoverError" type="checkbox" checked />
</label>
<label
class="innerControls"
title="Stop loading and playback if playback under-buffer stalls. This can help debug stall errors."
>
Stop on first stall:
<input id="stopOnStall" type="checkbox" unchecked />
</label>
<label class="innerControls">
Dump transmuxed fMP4 data:
<input id="dumpfMP4" type="checkbox" unchecked />
</label>
<label class="innerControls">
Metrics history (max limit, -1 is unlimited):
<input id="limitMetrics" style="width: 8em" type="number" />
</label>
<label class="innerControls">
HTML video element width:
<select id="videoSize" style="float: right">
<option value="240px">240px</option>
<option value="426px">426px</option>
<option value="640px">640px</option>
<option value="720px">720px</option>
<option value="854px">854px</option>
<option value="1280px">1280px</option>
<option value="1920px">1920px</option>
<option value="80%">Responsive (80%)</option>
<option value="100%">Responsive (100%)</option>
</select>
</label>
<label class="innerControls">
Current player size:
<span id="currentSize"></span>
</label>
<label class="innerControls">
Current video resolution:
<span id="currentResolution"></span>
</label>
<label class="innerControls permalink">
Permalink:&nbsp;&nbsp;<span id="StreamPermalink"></span>
</label>
</div>
<div class="config-editor-wrapper">
<div class="config-editor-container">
<div id="config-editor">Loading...</div>
</div>
<div class="config-editor-commands">
<label for="config-persistence">
Persist
<input
name="config-persistence"
id="config-persistence"
type="checkbox"
/>
</label>
<button name="config-apply" onclick="applyConfigEditorValue()">
Apply
</button>
</div>
</div>
</div>
<video
id="video"
controls
autoplay
class="videoCentered"
style="width: 80%"
></video>
<br />
<canvas
id="bufferedCanvas"
width="720"
height="15"
class="videoCentered"
onclick="onClickBufferedRange(event);"
style="height: fit-content"
></canvas>
<br />
<br />
<label class="center">Status:</label>
<pre id="statusOut" class="center" style="white-space: pre-wrap"></pre>
<label class="center">Error:</label>
<pre id="errorOut" class="center" style="white-space: pre-wrap"></pre>
<div
class="center"
style="text-align: center; display: none"
id="toggleButtons"
>
<button
type="button"
class="btn btn-sm demo-tab-btn"
data-tab="playbackControlTab"
onclick="toggleTabClick(this);"
>
Playback
</button>
<button
type="button"
class="btn btn-sm demo-tab-btn"
data-tab="timelineTab"
onclick="toggleTabClick(this);"
>
Timeline
</button>
<button
type="button"
class="btn btn-sm demo-tab-btn"
data-tab="qualityLevelControlTab"
onclick="toggleTabClick(this);"
>
Quality-levels
</button>
<button
type="button"
class="btn btn-sm demo-tab-btn"
data-tab="audioTrackControlTab"
onclick="toggleTabClick(this);"
>
Audio-tracks
</button>
<button
type="button"
class="btn btn-sm demo-tab-btn"
data-tab="metricsDisplayTab"
onclick="toggleTabClick(this); showMetrics();"
>
Real-time metrics
</button>
<button
type="button"
class="btn btn-sm demo-tab-btn"
data-tab="statsDisplayTab"
onclick="toggleTabClick(this);"
>
Buffer &amp; Statistics
</button>
</div>
<div
class="center demo-tab"
id="playbackControlTab"
style="display: none"
>
<br />
<center>
<p>
<span>
<button
type="button"
class="btn btn-sm btn-info"
title="video.play()"
onclick="$('#video')[0].play()"
>
Play
</button>
<button
type="button"
class="btn btn-sm btn-info"
title="video.pause()"
onclick="$('#video')[0].pause()"
>
Pause
</button>
</span>
<span>
<button
type="button"
class="btn btn-sm btn-info"
title="video.playbackRate = text input"
onclick="$('#video')[0].defaultPlaybackRate=$('#video')[0].playbackRate=$('#playback_rate').val();"
>
Playback rate
</button>
<input
type="number"
value="1"
id="playback_rate"
size="8"
style="width: 3em"
onkeydown="if(window.event.keyCode=='13'){$('#video')[0].defaultPlaybackRate=$('#video')[0].playbackRate=$('#playback_rate').val();}"
/>
</span>
<span>
<button
type="button"
class="btn btn-sm btn-info"
title="video.currentTime -= 10"
onclick="$('#video')[0].currentTime-=10"
>
- 10 s
</button>
<button
type="button"
class="btn btn-sm btn-info"
title="video.currentTime += 10"
onclick="$('#video')[0].currentTime+=10"
>
+ 10 s
</button>
</span>
<span>
<button
type="button"
class="btn btn-sm btn-info"
title="video.currentTime = text input"
onclick="$('#video')[0].currentTime=$('#seek_pos').val();"
>
Seek to
</button>
<input
type="number"
id="seek_pos"
size="8"
style="width: 7em"
onkeydown="if(window.event.keyCode=='13'){$('#video')[0].currentTime=$('#seek_pos').val();}"
/>
</span>
</p>
<p>
<span>
<button
type="button"
class="btn btn-xs btn-warning"
title="hls.startLoad()"
onclick="hls.startLoad()"
>
Start loading
</button>
<button
type="button"
class="btn btn-xs btn-warning"
title="hls.stopLoad()"
onclick="hls.stopLoad()"
>
Stop loading
</button>
</span>
<span>
<button
type="button"
class="btn btn-xs btn-warning"
title="hls.attachMedia(video)"
onclick="hls.attachMedia($('#video')[0])"
>
Attach media
</button>
<button
type="button"
class="btn btn-xs btn-warning"
title="hls.detachMedia()"
onclick="hls.detachMedia()"
>
Detach media
</button>
</span>
</p>
<p>
<span>
<button
type="button"
class="btn btn-xs btn-warning"
title="hls.recoverMediaError()"
onclick="hls.recoverMediaError()"
>
Recover media-error
</button>
<button
type="button"
class="btn btn-xs btn-warning"
title="hls.swapAudioCodec()"
onclick="hls.swapAudioCodec()"
>
Swap audio codec
</button>
</span>
</p>
<p>
<span>
<button
type="button"
class="btn btn-xs btn-default"
onclick="$('#streamSelect')[0].selectedIndex++;$('#streamSelect').change()"
>
Next video
</button>
<button
type="button"
class="btn btn-xs btn-default btn-dump"
title="Save dumped audio mp4 appends"
onclick="createfMP4('audio');"
>
Create audio-fmp4
</button>
<button
type="button"
class="btn btn-xs btn-default btn-dump"
title="Save dumped video mp4 appends"
onclick="createfMP4('video')"
>
Create video-fmp4
</button>
</span>
</p>
</center>
</div>
<div
class="center demo-tab demo-timeline-chart-container"
id="timelineTab"
style="display: none"
>
<canvas id="timeline-chart" style="touch-action: manipulation"></canvas>
</div>
<div
class="center demo-tab"
id="qualityLevelControlTab"
style="display: none"
>
<center>
<table>
<tr>
<td>
<p>Currently played level:</p>
</td>
<td>
<div id="currentLevelControl" style="display: inline"></div>
</td>
</tr>
<tr>
<td>
<p>Next level loaded:</p>
</td>
<td>
<div id="nextLevelControl" style="display: inline"></div>
</td>
</tr>
<tr>
<td>
<p>Currently loaded level:</p>
</td>
<td>
<div id="loadLevelControl" style="display: inline"></div>
</td>
</tr>
<tr>
<td>
<p>Cap-limit level (maximum):</p>
</td>
<td>
<div id="levelCappingControl" style="display: inline"></div>
</td>
</tr>
</table>
</center>
</div>
<div
class="center demo-tab"
id="audioTrackControlTab"
style="display: none"
>
<table>
<tr>
<td>Current audio-track:</td>
<td><div id="audioTrackControl" style="display: inline"></div></td>
</tr>
<tr>
<td>Language / Name:</td>
<td>
<div id="audioTrackLabel" style="display: inline">
None selected
</div>
</td>
</tr>
</table>
</div>
<div class="center demo-tab" id="metricsDisplayTab" style="display: none">
<br />
<div id="metricsButton">
<button
type="button"
class="btn btn-xs btn-info"
onclick="$('#metricsButtonWindow').toggle();$('#metricsButtonFixed').toggle();windowSliding=!windowSliding; refreshCanvas()"
>
toggle sliding/fixed window</button
><br />
<div id="metricsButtonWindow">
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(0)"
>
window ALL
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(2000)"
>
2s
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(5000)"
>
5s
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(10000)"
>
10s
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(20000)"
>
20s
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(30000)"
>
30s
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(60000)"
>
60s
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSetSliding(120000)"
>
120s</button
><br />
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeZoomIn()"
>
Window Zoom In
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeZoomOut()"
>
Window Zoom Out</button
><br />
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSlideLeft()"
>
<<< Window Slide
</button>
<button
type="button"
class="btn btn-xs btn-info"
onclick="timeRangeSlideRight()"
>
Window Slide >>></button
><br />
</div>
<div id="metricsButtonFixed">
<button
type="button"
class="btn btn-xs btn-info"
onclick="windowStart=$('#windowStart').val()"
>
fixed window start(ms)
</button>
<input
type="text"
id="windowStart"
defaultValue="0"
size="8"
onkeydown="if(window.event.keyCode=='13'){windowStart=$('#windowStart').val();}"
/>
<button
type="button"
class="btn btn-xs btn-info"
onclick="windowEnd=$('#windowEnd').val()"
>
fixed window end(ms)
</button>
<input
type="text"
id="windowEnd"
defaultValue="10000"
size="8"
onkeydown="if(window.event.keyCode=='13'){windowEnd=$('#windowEnd').val();}"
/><br />
</div>
<button
type="button"
class="btn btn-xs btn-success"
onclick="goToMetrics()"
style="font-size: 18px"
>
metrics link
</button>
<button
type="button"
class="btn btn-xs btn-success"
onclick="goToMetricsPermaLink()"
style="font-size: 18px"
>
metrics permalink
</button>
<button
type="button"
class="btn btn-xs btn-success"
onclick="copyMetricsToClipBoard()"
style="font-size: 18px"
>
copy metrics to clipboard
</button>
<canvas
id="bufferTimerange_c"
width="640"
height="100"
style="border: 1px solid #000000"
onmousedown="timeRangeCanvasonMouseDown(event)"
onmousemove="timeRangeCanvasonMouseMove(event)"
onmouseup="timeRangeCanvasonMouseUp(event)"
onmouseout="timeRangeCanvasonMouseOut(event);"
></canvas>
<canvas
id="bitrateTimerange_c"
width="640"
height="100"
style="border: 1px solid #000000"
></canvas>
<canvas
id="bufferWindow_c"
width="640"
height="100"
style="border: 1px solid #000000"
onmousemove="windowCanvasonMouseMove(event);"
></canvas>
<canvas
id="videoEvent_c"
width="640"
height="15"
style="border: 1px solid #000000"
></canvas>
<canvas
id="loadEvent_c"
width="640"
height="15"
style="border: 1px solid #000000"
></canvas
><br />
</div>
</div>
<div class="center demo-tab" id="statsDisplayTab" style="display: none">
<br />
<label>Buffer state:</label>
<pre id="bufferedOut"></pre>
<label>General stats:</label>
<pre id="statisticsOut"></pre>
</div>
</div>
<footer><br /><br /><br /><br /><br /><br /></footer>
<!-- Demo page required libs -->
<script src="canvas.js"></script>
<script src="metrics.js"></script>
<!-- demo build -->
<script src="hls.js"></script>
<script src="hls-demo.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+228
View File
@@ -0,0 +1,228 @@
<!DOCTYPE html>
<head>
<title>hls.js metrics page</title>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"
/>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap-theme.min.css"
/>
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<script src="../node_modules/jsonpack/main.js"></script>
</head>
<body>
<div class="header-container">
<header class="wrapper clearfix">
<h1 class="title">hls.js metrics page</h1>
</header>
</div>
<pre id="HlsDate"></pre>
<pre id="StreamPermalink"></pre>
<input id="metricsData" class="innerControls" type="text" value="" />
window size
<div id="metricsButton">
<button
type="button"
class="btn btn-xs btn-primary"
onclick="timeRangeSetSliding(0)"
>
window ALL
</button>
<button
type="button"
class="btn btn-xs btn-primary"
onclick="timeRangeSetSliding(2000)"
>
2s
</button>
<button
type="button"
class="btn btn-xs btn-primary"
onclick="timeRangeSetSliding(5000)"
>
5s
</button>
<button
type="button"
class="btn btn-xs btn-primary"
onclick="timeRangeSetSliding(10000)"
>
10s
</button>
<button
type="button"
class="btn btn-xs btn-primary"
onclick="timeRangeSetSliding(20000)"
>
20s
</button>
<button
type="button"
class="btn btn-xs btn-primary"
onclick="timeRangeSetSliding(30000)"
>
30s
</button>
<button
type="button"
class="btn btn-xs btn-primary"
onclick="timeRangeSetSliding(60000)"
>
60s
</button>
<button
type="button"
class="btn btn-xs btn-primary"
onclick="timeRangeSetSliding(120000)"
>
120s</button
><br />
<button
type="button"
class="btn btn-xs btn-primary"
onclick="timeRangeZoomIn()"
>
Window Zoom In
</button>
<button
type="button"
class="btn btn-xs btn-primary"
onclick="timeRangeZoomOut()"
>
Window Zoom Out</button
><br />
<button
type="button"
class="btn btn-xs btn-primary"
onclick="timeRangeSlideLeft()"
>
<<< Window Slide
</button>
<button
type="button"
class="btn btn-xs btn-primary"
onclick="timeRangeSlideRight()"
>
Window Slide >>></button
><br />
<button
type="button"
class="btn btn-xs btn-primary"
onclick="windowStart=$('#windowStart').val()"
>
fixed window start(ms)
</button>
<input
type="text"
id="windowStart"
defaultValue="0"
size="8"
onkeydown="if(window.event.keyCode=='13'){windowStart=$('#windowStart').val();}"
/>
<button
type="button"
class="btn btn-xs btn-primary"
onclick="windowEnd=$('#windowEnd').val()"
>
fixed window end(ms)
</button>
<input
type="text"
id="windowEnd"
defaultValue="10000"
size="8"
onkeydown="if(window.event.keyCode=='13'){windowEnd=$('#windowEnd').val();}"
/><br />
<canvas
id="bufferTimerange_c"
width="640"
height="100"
style="border: 1px solid #000000"
onmousedown="timeRangeCanvasonMouseDown(event)"
onmousemove="timeRangeCanvasonMouseMove(event)"
onmouseup="timeRangeCanvasonMouseUp(event)"
onmouseout="timeRangeCanvasonMouseOut(event)"
;
></canvas>
<canvas
id="bitrateTimerange_c"
width="640"
height="100"
style="border: 1px solid #000000"
;
></canvas>
<canvas
id="bufferWindow_c"
width="640"
height="100"
style="border: 1px solid #000000"
onmousemove="windowCanvasonMouseMove(event)"
;
></canvas>
<canvas
id="videoEvent_c"
width="640"
height="15"
style="border: 1px solid #000000"
;
></canvas>
<canvas
id="loadEvent_c"
width="640"
height="15"
style="border: 1px solid #000000"
;
></canvas
><br />
</div>
<script src="canvas.js"></script>
<script src="metrics.js"></script>
<script>
$(document).ready(function () {
$('#metricsData').change(function () {
events = jsonpack.unpack(atob($('#metricsData').val()));
updateMetrics();
});
});
var data =
location.search.split('data=')[1] || location.hash.split('data=')[1],
events;
if (data) {
events = jsonpack.unpack(atob(decodeURIComponent(data)));
updateMetrics();
}
function updateMetrics() {
var hlsLink = new URL(
'index.html?src=' + encodeURIComponent(events.url),
window.location.href
).href;
var playlistLink = document.createElement('a');
playlistLink.setAttribute('href', events.url);
playlistLink.textContent = events.url;
var replayLink = document.createElement('a');
replayLink.setAttribute('href', hlsLink);
replayLink.textContent = hlsLink;
var fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode('playlist: '));
fragment.appendChild(playlistLink);
fragment.appendChild(document.createElement('br'));
fragment.appendChild(document.createTextNode('replay: '));
fragment.appendChild(replayLink);
$('#StreamPermalink').html(fragment);
$('#HlsDate').text('session Start Date:' + new Date(events.t0));
metricsDisplayed = true;
showMetrics();
refreshCanvas();
}
</script>
</body>
+266
View File
@@ -0,0 +1,266 @@
/* global $, events, eventLeftMargin, canvasBufferTimeRangeUpdate, canvasBufferWindowUpdate, canvasBitrateEventUpdate,
canvasVideoEventUpdate, canvasLoadEventUpdate */
/* eslint no-var: 0 */
var windowDuration = 20000;
var windowSliding = true;
var windowStart = 0;
var windowEnd = 10000;
var windowFocus;
var metricsDisplayed = false;
var timeRangeMouseDown = false;
$('#windowStart').val(windowStart);
$('#windowEnd').val(windowEnd);
function showMetrics() {
metricsDisplayed = true;
var width = window.innerWidth - 30;
$('#bufferWindow_c')[0].width =
$('#bitrateTimerange_c')[0].width =
$('#bufferTimerange_c')[0].width =
$('#videoEvent_c')[0].width =
$('#metricsButton')[0].width =
$('#loadEvent_c')[0].width =
width;
$('#bufferWindow_c').show();
$('#bitrateTimerange_c').show();
$('#bufferTimerange_c').show();
$('#videoEvent_c').show();
$('#metricsButton').show();
$('#loadEvent_c').show();
}
function hideMetrics() {
metricsDisplayed = false;
$('#bufferWindow_c').hide();
$('#bitrateTimerange_c').hide();
$('#bufferTimerange_c').hide();
$('#videoEvent_c').hide();
$('#metricsButton').hide();
$('#loadEvent_c').hide();
}
function timeRangeSetSliding(duration) {
windowDuration = duration;
windowSliding = true;
refreshCanvas();
}
function timeRangeCanvasonMouseDown(evt) {
var canvas = evt.currentTarget;
var bRect = canvas.getBoundingClientRect();
var mouseX = Math.round(
(evt.clientX - bRect.left) * (canvas.width / bRect.width)
);
windowStart = Math.max(
0,
Math.round(
((mouseX - eventLeftMargin) * getWindowTimeRange().now) /
(canvas.width - eventLeftMargin)
)
);
windowEnd = windowStart + 500;
timeRangeMouseDown = true;
windowSliding = false;
// console.log('windowStart/windowEnd:' + '/' + windowStart + '/' + windowEnd);
$('#windowStart').val(windowStart);
$('#windowEnd').val(windowEnd);
refreshCanvas();
}
function timeRangeCanvasonMouseMove(evt) {
if (timeRangeMouseDown) {
var canvas = evt.currentTarget;
var bRect = canvas.getBoundingClientRect();
var mouseX = Math.round(
(evt.clientX - bRect.left) * (canvas.width / bRect.width)
);
var pos = Math.max(
0,
Math.round(
((mouseX - eventLeftMargin) * getWindowTimeRange().now) /
(canvas.width - eventLeftMargin)
)
);
if (pos < windowStart) {
windowStart = pos;
} else {
windowEnd = pos;
}
if (windowStart === windowEnd) {
// to avoid division by zero ...
windowEnd += 50;
}
// console.log('windowStart/windowEnd:' + '/' + windowStart + '/' + windowEnd);
$('#windowStart').val(windowStart);
$('#windowEnd').val(windowEnd);
refreshCanvas();
}
}
function timeRangeCanvasonMouseUp(evt) {
timeRangeMouseDown = false;
}
function timeRangeCanvasonMouseOut(evt) {
timeRangeMouseDown = false;
}
function windowCanvasonMouseMove(evt) {
var canvas = evt.currentTarget;
var bRect = canvas.getBoundingClientRect();
var mouseX = Math.round(
(evt.clientX - bRect.left) * (canvas.width / bRect.width)
);
var timeRange = getWindowTimeRange();
windowFocus =
timeRange.min +
Math.max(
0,
Math.round(
((mouseX - eventLeftMargin) * (timeRange.max - timeRange.min)) /
(canvas.width - eventLeftMargin)
)
);
// console.log(windowFocus);
refreshCanvas();
}
function refreshCanvas() {
if (metricsDisplayed) {
try {
var windowTime = getWindowTimeRange();
canvasBufferTimeRangeUpdate(
$('#bufferTimerange_c')[0],
0,
windowTime.now,
windowTime.min,
windowTime.max,
events.buffer
);
if (windowTime.min !== 0 || windowTime.max !== windowTime.now) {
$('#bufferWindow_c').show();
canvasBufferWindowUpdate(
$('#bufferWindow_c')[0],
windowTime.min,
windowTime.max,
windowTime.focus,
events.buffer
);
} else {
$('#bufferWindow_c').hide();
}
canvasBitrateEventUpdate(
$('#bitrateTimerange_c')[0],
0,
windowTime.now,
windowTime.min,
windowTime.max,
events.level,
events.bitrate
);
canvasVideoEventUpdate(
$('#videoEvent_c')[0],
windowTime.min,
windowTime.max,
events.video
);
canvasLoadEventUpdate(
$('#loadEvent_c')[0],
windowTime.min,
windowTime.max,
events.load
);
} catch (err) {
console.log('refreshCanvas error:' + err.message);
}
}
}
function getWindowTimeRange() {
var tnow;
var minTime;
var maxTime;
if (events.buffer.length) {
tnow = events.buffer[events.buffer.length - 1].time;
} else {
tnow = 0;
}
if (windowSliding) {
// let's show the requested window
if (windowDuration) {
minTime = Math.max(0, tnow - windowDuration);
maxTime = Math.min(minTime + windowDuration, tnow);
} else {
minTime = 0;
maxTime = tnow;
}
} else {
minTime = windowStart;
maxTime = windowEnd;
}
if (
windowFocus === undefined ||
windowFocus < minTime ||
windowFocus > maxTime
) {
windowFocus = minTime;
}
return { min: minTime, max: maxTime, now: tnow, focus: windowFocus };
}
function timeRangeZoomIn() {
if (windowSliding) {
windowDuration /= 2;
} else {
var duration = windowEnd - windowStart;
windowStart += duration / 4;
windowEnd -= duration / 4;
if (windowStart === windowEnd) {
windowEnd += 50;
}
}
$('#windowStart').val(windowStart);
$('#windowEnd').val(windowEnd);
refreshCanvas();
}
function timeRangeZoomOut() {
if (windowSliding) {
windowDuration *= 2;
} else {
var duration = windowEnd - windowStart;
windowStart -= duration / 2;
windowEnd += duration / 2;
windowStart = Math.max(0, windowStart);
windowEnd = Math.min(
events.buffer[events.buffer.length - 1].time,
windowEnd
);
}
$('#windowStart').val(windowStart);
$('#windowEnd').val(windowEnd);
refreshCanvas();
}
function timeRangeSlideLeft() {
var duration = windowEnd - windowStart;
windowStart -= duration / 4;
windowEnd -= duration / 4;
windowStart = Math.max(0, windowStart);
windowEnd = Math.min(events.buffer[events.buffer.length - 1].time, windowEnd);
$('#windowStart').val(windowStart);
$('#windowEnd').val(windowEnd);
refreshCanvas();
}
function timeRangeSlideRight() {
var duration = windowEnd - windowStart;
windowStart += duration / 4;
windowEnd += duration / 4;
windowStart = Math.max(0, windowStart);
windowEnd = Math.min(events.buffer[events.buffer.length - 1].time, windowEnd);
$('#windowStart').val(windowStart);
$('#windowEnd').val(windowEnd);
refreshCanvas();
}
+240
View File
@@ -0,0 +1,240 @@
header {
text-align: center;
}
th,
td {
padding: 15px;
}
select {
padding: 2px 15px;
background-color: rgb(181, 222, 255);
font-weight: 600;
padding: 5px 0;
}
select option {
font-size: 11px;
}
.innerControls input {
font-weight: normal;
float: right;
}
.innerControls.permalink {
display: inline-block;
}
@media (prefers-color-scheme: dark) {
body {
color: #ddd;
background-color: #111;
}
button,
optgroup,
select {
background-color: rgb(0, 40, 70);
}
input,
textarea {
background-color: #222;
}
label {
font-weight: initial;
}
a {
color: #337ab7;
}
pre {
background-color: #050505;
border-color: #333;
color: #ccc;
}
.ace-github {
background: #000;
color: #fff;
}
.ace-github .ace_gutter {
background: #181818;
color: #888;
}
.ace-github .ace_marker-layer .ace_active-line {
background: #111;
}
.ace-github.ace_focus .ace_marker-layer .ace_active-line {
background: #321;
}
div.config-editor-commands {
background-color: #444;
border-color: #333;
}
button.btn {
color: #050505;
}
canvas {
background: #bbb;
}
}
#controls {
display: flex;
flex-direction: column;
width: 80%;
max-width: 1200px;
margin: 0 auto 20px auto;
border: 1px solid #606060;
overflow: hidden;
}
.demo-controls-wrapper {
flex: 1 1 auto;
max-width: 100%;
padding: 5px 5px 0 3px;
}
.config-editor-wrapper {
flex: 1 1 auto;
display: flex;
flex-direction: column;
border-top: solid 1px #ccc;
height: 256px;
}
.config-editor-container {
flex: 1 1 auto;
position: relative;
width: 100%;
height: 100%;
}
#config-editor {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.config-editor-commands {
flex: 1 1 auto;
background-color: #ddd;
border-top: solid 1px #ccc;
padding: 5px;
display: flex;
justify-content: space-between;
align-items: center;
}
.config-editor-commands label {
margin-bottom: 0;
}
.config-editor-commands button {
padding: 5px 8px;
font-size: 14px;
}
.innerControls {
display: flex;
font-size: 12px;
align-items: center;
margin-bottom: 5px;
padding-left: 5px;
justify-content: space-between;
}
.videoCentered {
width: 720px;
margin-left: auto;
margin-right: auto;
display: block;
}
.center {
width: 70%;
min-width: 615px;
overflow: hidden;
margin-left: auto;
margin-right: auto;
display: block;
}
#toggleButtons button {
width: 16%;
display: inline-block;
text-align: center;
font-size: 10pt;
font-weight: bolder;
background-color: rgb(181, 222, 255);
padding: 5px;
overflow: hidden;
text-overflow: ellipsis;
}
#statusOut {
height: auto;
max-height: calc((17px * 3) + 19px);
overflow: auto;
}
#errorOut {
height: auto;
max-height: calc((17px * 3) + 19px);
overflow: auto;
}
#streamURL,
#streamSelect {
width: calc(100% - 4px);
margin-left: 3px;
}
#streamURL {
margin-bottom: 10px;
padding-left: 3px;
}
#streamSelect {
padding: 5px 0;
}
#StreamPermalink {
overflow-wrap: break-word;
overflow: hidden; /* for IE11 */
}
#StreamPermalink a {
font-size: 10px;
font-family: monospace;
}
/* Small devices (portrait tablets and large phones, 600px and up) */
@media only screen and (min-width: 600px) {
#controls {
flex-direction: row;
}
.demo-controls-wrapper {
max-width: 50%;
}
.config-editor-wrapper {
height: auto;
border-top: 0;
border-left: solid 1px #ccc;
}
}
+107 -2
View File
@@ -1,12 +1,117 @@
package plugin_hls
import (
"embed"
"net/http"
"path"
"strings"
"time"
"m7s.live/v5"
"m7s.live/v5/plugin/hls/pkg"
hls "m7s.live/v5/plugin/hls/pkg"
)
var _ = m7s.InstallPlugin[HLSPlugin](hls.NewPuller)
var _ = m7s.InstallPlugin[HLSPlugin](hls.NewPuller, hls.NewTransform)
//go:embed hls.js
var hls_js embed.FS
type HLSPlugin struct {
m7s.Plugin
}
func (p *HLSPlugin) OnDeviceAdd(device *m7s.Device) any {
d := &hls.HLSDevice{}
d.Device = device
d.Plugin = &p.Plugin
return d
}
func (config *HLSPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fileName := strings.TrimPrefix(r.URL.Path, "/")
query := r.URL.Query()
waitTimeout, err := time.ParseDuration(query.Get("timeout"))
if err == nil {
config.Debug("request", "fileName", fileName, "timeout", waitTimeout)
} else {
waitTimeout = time.Second * 10
}
waitStart := time.Now()
if strings.HasSuffix(r.URL.Path, ".m3u8") {
w.Header().Add("Content-Type", "application/vnd.apple.mpegurl")
streamPath := strings.TrimSuffix(fileName, ".m3u8")
for {
if v, ok := hls.MemoryM3u8.Load(streamPath); ok {
w.Write([]byte(v.(string)))
return
}
if waitTimeout > 0 && time.Since(waitStart) < waitTimeout {
config.Server.OnSubscribe(streamPath, r.URL.Query())
time.Sleep(time.Second)
continue
} else {
break
}
}
// w.Write([]byte(fmt.Sprintf(`#EXTM3U
// #EXT-X-VERSION:3
// #EXT-X-MEDIA-SEQUENCE:%d
// #EXT-X-TARGETDURATION:%d
// #EXT-X-DISCONTINUITY-SEQUENCE:%d
// #EXT-X-DISCONTINUITY
// #EXTINF:%.3f,
// default.ts`, defaultSeq, int(math.Ceil(config.DefaultTSDuration.Seconds())), defaultSeq, config.DefaultTSDuration.Seconds())))
} else if strings.HasSuffix(r.URL.Path, ".ts") {
w.Header().Add("Content-Type", "video/mp2t") //video/mp2t
streamPath := path.Dir(fileName)
for {
tsData, ok := hls.MemoryTs.Load(streamPath)
if !ok {
tsData, ok = hls.MemoryTs.Load(path.Dir(streamPath))
if !ok {
if waitTimeout > 0 && time.Since(waitStart) < waitTimeout {
time.Sleep(time.Second)
continue
} else {
// w.Write(defaultTS)
return
}
}
}
for {
if tsData, ok := tsData.(hls.TsCacher).GetTs(fileName); ok {
switch v := tsData.(type) {
case *hls.TsInMemory:
v.WriteTo(w)
case []byte:
w.Write(v)
}
return
} else {
if waitTimeout > 0 && time.Since(waitStart) < waitTimeout {
time.Sleep(time.Second)
continue
} else {
// w.Write(defaultTS)
return
}
}
}
}
} else {
f, err := hls_js.ReadFile("hls.js/" + fileName)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
} else {
w.Write(f)
}
// if file, err := hls_js.Open(fileName); err == nil {
// defer file.Close()
// if info, err := file.Stat(); err == nil {
// http.ServeContent(w, r, fileName, info.ModTime(), file)
// }
// } else {
// http.NotFound(w, r)
// }
}
}
+5
View File
@@ -0,0 +1,5 @@
package hls
import "m7s.live/v5"
type HLSDevice = m7s.HTTPDevice
+5 -2
View File
@@ -11,9 +11,12 @@ import (
"github.com/quangngotan95/go-m3u8/m3u8"
)
var memoryM3u8 sync.Map
var memoryTs sync.Map
var MemoryM3u8 sync.Map
var MemoryTs sync.Map
type TsCacher interface {
GetTs(key string) (any, bool)
}
type M3u8Info struct {
Req *http.Request
M3U8Count int //一共拉取的m3u8文件数量
+3 -3
View File
@@ -51,14 +51,14 @@ func (p *Puller) Start() (err error) {
}
p.PullJob.Publisher.Speed = 1
if p.PullJob.PublishConfig.RelayMode != config.RelayModeRemux {
p.memoryTs.Store(p.PullJob.StreamPath, p)
MemoryTs.Store(p.PullJob.StreamPath, p)
}
return
}
func (p *Puller) Dispose() {
if p.PullJob.PublishConfig.RelayMode == config.RelayModeRelay {
memoryTs.Delete(p.PullJob.StreamPath)
MemoryTs.Delete(p.PullJob.StreamPath)
}
}
@@ -310,7 +310,7 @@ func (p *Puller) pull(info *M3u8Info) (err error) {
if p.PullJob.PublishConfig.RelayMode != config.RelayModeRemux {
m3u8 := string(plBuffer)
p.Debug("write m3u8", "streamPath", p.PullJob.StreamPath, "m3u8", m3u8)
memoryM3u8.Store(p.PullJob.StreamPath, m3u8)
MemoryM3u8.Store(p.PullJob.StreamPath, m3u8)
}
} else {
p.Error("readM3u8", "streamPath", p.PullJob.StreamPath, "err", err2)
+176
View File
@@ -0,0 +1,176 @@
package hls
import (
"errors"
"fmt"
"io"
"net"
"slices"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
mpegts "m7s.live/v5/plugin/hls/pkg/ts"
)
type TsInMemory struct {
PMT util.Buffer
util.RecyclableMemory
}
func (ts *TsInMemory) WritePMTPacket(audio, video codec.FourCC) {
ts.PMT.Reset()
mpegts.WritePMTPacket(&ts.PMT, video, audio)
}
func (ts *TsInMemory) WriteTo(w io.Writer) (int64, error) {
w.Write(mpegts.DefaultPATPacket)
w.Write(ts.PMT)
cloneBuffers := slices.Clone(ts.Buffers)
return cloneBuffers.WriteTo(w)
}
func (ts *TsInMemory) WritePESPacket(frame *mpegts.MpegtsPESFrame, packet mpegts.MpegTsPESPacket) (err error) {
if packet.Header.PacketStartCodePrefix != 0x000001 {
err = errors.New("packetStartCodePrefix != 0x000001")
return
}
var pesHeadItem util.Buffer = ts.GetAllocator().Borrow(32)
pesHeadItem.Reset()
_, err = mpegts.WritePESHeader(&pesHeadItem, packet.Header)
if err != nil {
return
}
pesBuffers := append(net.Buffers{pesHeadItem}, packet.Buffers...)
pesPktLength := int64(util.SizeOfBuffers(pesBuffers))
var tsHeaderLength int
for i := 0; pesPktLength > 0; i++ {
var buffer util.Buffer = ts.NextN(mpegts.TS_PACKET_SIZE)
bwTsHeader := &buffer
bwTsHeader.Reset()
tsHeader := mpegts.MpegTsHeader{
SyncByte: 0x47,
TransportErrorIndicator: 0,
PayloadUnitStartIndicator: 0,
TransportPriority: 0,
Pid: frame.Pid,
TransportScramblingControl: 0,
AdaptionFieldControl: 1,
ContinuityCounter: frame.ContinuityCounter,
}
frame.ContinuityCounter++
frame.ContinuityCounter = frame.ContinuityCounter % 16
// 每一帧的开头,当含有pcr的时候,包含调整字段
if i == 0 {
tsHeader.PayloadUnitStartIndicator = 1
// 当PCRFlag为1的时候,包含调整字段
if frame.IsKeyFrame {
tsHeader.AdaptionFieldControl = 0x03
tsHeader.AdaptationFieldLength = 7
tsHeader.PCRFlag = 1
tsHeader.RandomAccessIndicator = 1
tsHeader.ProgramClockReferenceBase = frame.ProgramClockReferenceBase
}
}
// 每一帧的结尾,当不满足188个字节的时候,包含调整字段
if pesPktLength < mpegts.TS_PACKET_SIZE-4 {
var tsStuffingLength uint8
tsHeader.AdaptionFieldControl = 0x03
tsHeader.AdaptationFieldLength = uint8(mpegts.TS_PACKET_SIZE - 4 - 1 - pesPktLength)
// TODO:如果第一个TS包也是最后一个TS包,是不是需要考虑这个情况?
// MpegTsHeader最少占6个字节.(前4个走字节 + AdaptationFieldLength(1 byte) + 3个指示符5个标志位(1 byte))
if tsHeader.AdaptationFieldLength >= 1 {
tsStuffingLength = tsHeader.AdaptationFieldLength - 1
} else {
tsStuffingLength = 0
}
// error
tsHeaderLength, err = mpegts.WriteTsHeader(bwTsHeader, tsHeader)
if err != nil {
return
}
if tsStuffingLength > 0 {
if _, err = bwTsHeader.Write(mpegts.Stuffing[:tsStuffingLength]); err != nil {
return
}
}
tsHeaderLength += int(tsStuffingLength)
} else {
tsHeaderLength, err = mpegts.WriteTsHeader(bwTsHeader, tsHeader)
if err != nil {
return
}
}
tsPayloadLength := mpegts.TS_PACKET_SIZE - tsHeaderLength
//fmt.Println("tsPayloadLength :", tsPayloadLength)
// 这里不断的减少PES包
written, _ := io.CopyN(bwTsHeader, &pesBuffers, int64(tsPayloadLength))
// tmp := tsHeaderByte[3] << 2
// tmp = tmp >> 6
// if tmp == 2 {
// fmt.Println("fuck you mother.")
// }
pesPktLength -= written
tsPktByteLen := bwTsHeader.Len()
if tsPktByteLen != mpegts.TS_PACKET_SIZE {
err = errors.New(fmt.Sprintf("%s, packet size=%d", "TS_PACKET_SIZE != 188,", tsPktByteLen))
return
}
}
return nil
}
func (ts *TsInMemory) WriteAudioFrame(frame *pkg.ADTS, pes *mpegts.MpegtsPESFrame) (err error) {
// packetLength = 原始音频流长度 + adts(7) + MpegTsOptionalPESHeader长度(8 bytes, 因为只含有pts)
var packet mpegts.MpegTsPESPacket
packet.Header.PesPacketLength = uint16(frame.Size + 8)
packet.Buffers = slices.Clone(frame.Buffers)
packet.Header.Pts = uint64(frame.DTS)
packet.Header.PacketStartCodePrefix = 0x000001
packet.Header.ConstTen = 0x80
packet.Header.StreamID = mpegts.STREAM_ID_AUDIO
pes.ProgramClockReferenceBase = packet.Header.Pts
packet.Header.PtsDtsFlags = 0x80
packet.Header.PesHeaderDataLength = 5
return ts.WritePESPacket(pes, packet)
}
func (ts *TsInMemory) WriteVideoFrame(frame *pkg.AnnexB, pes *mpegts.MpegtsPESFrame) (err error) {
var buffer net.Buffers
//需要对原始数据(ES),进行一些预处理,视频需要分割nalu(H264编码),并且打上sps,pps,nalu_aud信息.
if frame.Hevc {
buffer = append(buffer, codec.AudNalu)
} else {
buffer = append(buffer, codec.NALU_AUD_BYTE)
}
buffer = append(buffer, frame.Buffers...)
pktLength := util.SizeOfBuffers(buffer) + 10 + 3
if pktLength > 0xffff {
pktLength = 0
}
var packet mpegts.MpegTsPESPacket
packet.Header.PacketStartCodePrefix = 0x000001
packet.Header.ConstTen = 0x80
packet.Header.StreamID = mpegts.STREAM_ID_VIDEO
packet.Header.PesPacketLength = uint16(pktLength)
packet.Header.Pts = uint64(frame.PTS)
pes.ProgramClockReferenceBase = packet.Header.Pts
packet.Header.Dts = uint64(frame.DTS)
packet.Header.PtsDtsFlags = 0xC0
packet.Header.PesHeaderDataLength = 10
packet.Buffers = buffer
return ts.WritePESPacket(pes, packet)
}
+186
View File
@@ -0,0 +1,186 @@
package hls
import (
"container/ring"
"fmt"
"math"
"strconv"
"strings"
"sync"
"time"
"m7s.live/v5"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
mpegts "m7s.live/v5/plugin/hls/pkg/ts"
)
func NewTransform() m7s.ITransformer {
ret := &HLSWriter{
Window: 3,
Fragment: 5 * time.Second,
}
return ret
}
type HLSWriter struct {
m7s.DefaultTransformer
Window int
Fragment time.Duration
M3u8 util.Buffer
ts *TsInMemory
pesAudio, pesVideo *mpegts.MpegtsPESFrame
write_time time.Duration
memoryTs sync.Map
hls_segment_count uint32 // hls segment count
playlist Playlist
infoRing *ring.Ring
hls_playlist_count uint32
hls_segment_window uint32
lastReadTime time.Time
}
func (w *HLSWriter) Start() (err error) {
return w.TransformJob.Subscribe()
}
func (w *HLSWriter) GetTs(key string) (any, bool) {
w.lastReadTime = time.Now()
return w.memoryTs.Load(key)
}
func (w *HLSWriter) Run() (err error) {
if conf, ok := w.TransformJob.Config.Input.(string); ok {
ss := strings.Split(conf, "x")
if len(ss) != 2 {
return fmt.Errorf("invalid input config %s", conf)
}
w.Fragment, err = time.ParseDuration(strings.TrimSpace(ss[0]))
if err != nil {
return
}
w.Window, err = strconv.Atoi(strings.TrimSpace(ss[1]))
if err != nil {
return
}
}
subscriber := w.TransformJob.Subscriber
w.hls_segment_window = uint32(w.Window) + 1
w.infoRing = ring.New(w.Window + 1)
w.playlist = Playlist{
Writer: &w.M3u8,
Version: 3,
Sequence: 0,
Targetduration: int(w.Fragment / time.Millisecond / 666), // hlsFragment * 1.5 / 1000
}
MemoryTs.Store(w.TransformJob.StreamPath, w)
var audioCodec, videoCodec codec.FourCC
if subscriber.Publisher.HasAudioTrack() {
audioCodec = subscriber.Publisher.AudioTrack.FourCC()
}
if subscriber.Publisher.HasVideoTrack() {
videoCodec = subscriber.Publisher.VideoTrack.FourCC()
}
w.ts = &TsInMemory{}
w.pesAudio = &mpegts.MpegtsPESFrame{
Pid: mpegts.PID_AUDIO,
}
w.pesVideo = &mpegts.MpegtsPESFrame{
Pid: mpegts.PID_VIDEO,
}
w.ts.WritePMTPacket(audioCodec, videoCodec)
return m7s.PlayBlock(subscriber, w.ProcessADTS, w.ProcessAnnexB)
}
func (w *HLSWriter) ProcessADTS(audio *pkg.ADTS) (err error) {
return w.ts.WriteAudioFrame(audio, w.pesAudio)
}
func (w *HLSWriter) ProcessAnnexB(video *pkg.AnnexB) (err error) {
if w.TransformJob.Subscriber.VideoReader.Value.IDR {
if err = w.checkFragment(video.GetTimestamp()); err != nil {
return
}
}
return w.ts.WriteVideoFrame(video, w.pesVideo)
}
func (w *HLSWriter) checkFragment(ts time.Duration) (err error) {
// 当前的时间戳减去上一个ts切片的时间戳
if dur := ts - w.write_time; dur >= w.Fragment {
streamPath := w.TransformJob.StreamPath
ss := strings.Split(streamPath, "/")
// fmt.Println("time :", video.Timestamp, tsSegmentTimestamp)
if dur == ts && w.write_time == 0 { //时间戳不对的情况,首个默认为2s
dur = time.Duration(2) * time.Second
}
num := w.hls_segment_count
tsFilename := strconv.FormatInt(time.Now().Unix(), 10) + "_" + strconv.FormatUint(uint64(num), 10) + ".ts"
tsFilePath := streamPath + "/" + tsFilename
// println(hls.currentTs.Length)
w.Debug("write ts", "tsFilePath", tsFilePath)
w.memoryTs.Store(tsFilePath, w.ts)
w.ts = &TsInMemory{
PMT: w.ts.PMT,
}
if w.playlist.Targetduration < int(dur.Seconds()) {
w.playlist.Targetduration = int(math.Ceil(dur.Seconds()))
}
if w.M3u8.Len() == 0 {
w.playlist.Init()
}
inf := PlaylistInf{
//浮点计算精度
Duration: dur.Seconds(),
Title: fmt.Sprintf("%s/%s", ss[len(ss)-1], tsFilename),
FilePath: tsFilePath,
}
if w.hls_segment_count > 0 {
if w.hls_playlist_count >= uint32(w.Window) {
w.M3u8.Reset()
if err = w.playlist.Init(); err != nil {
return
}
//playlist起点是ring.next,长度是len(ring)-1
for p := w.infoRing.Next(); p != w.infoRing; p = p.Next() {
w.playlist.WriteInf(p.Value.(PlaylistInf))
}
} else {
if err = w.playlist.WriteInf(w.infoRing.Prev().Value.(PlaylistInf)); err != nil {
return
}
}
MemoryM3u8.Store(w.TransformJob.StreamPath, string(w.M3u8))
w.hls_playlist_count++
}
if w.hls_segment_count >= w.hls_segment_window {
if mts, loaded := w.memoryTs.LoadAndDelete(w.infoRing.Value.(PlaylistInf).FilePath); loaded {
mts.(*TsInMemory).Recycle()
}
w.infoRing.Value = inf
w.infoRing = w.infoRing.Next()
} else {
w.infoRing.Value = inf
w.infoRing = w.infoRing.Next()
}
w.hls_segment_count++
w.write_time = ts
}
return
}
func (w *HLSWriter) Dispose() {
MemoryM3u8.Delete(w.TransformJob.StreamPath)
if ts, loaded := MemoryTs.LoadAndDelete(w.TransformJob.StreamPath); loaded {
ts.(*HLSWriter).memoryTs.Range(func(key, value any) bool {
value.(*TsInMemory).Recycle()
return true
})
}
}
+1 -14
View File
@@ -64,14 +64,6 @@ func (s *Sender) sendADTS(audio *pkg.ADTS) (err error) {
return s.sendAudio(packet)
}
func (s *Sender) sendRawAudio(audio *pkg.RawAudio) (err error) {
var packet mpegts.MpegTsPESPacket
packet.Header.PesPacketLength = uint16(audio.Size + 8)
packet.Buffers = audio.Buffers
packet.Header.Pts = uint64(audio.Timestamp)
return s.sendAudio(packet)
}
func (s *Sender) sendVideo(video *pkg.AnnexB) (err error) {
var buffer net.Buffers
//需要对原始数据(ES),进行一些预处理,视频需要分割nalu(H264编码),并且打上sps,pps,nalu_aud信息.
@@ -101,12 +93,7 @@ func (s *Sender) sendVideo(video *pkg.AnnexB) (err error) {
}
func (s *Sender) Go() error {
if s.Subscriber.Publisher.HasAudioTrack() {
if s.Subscriber.Publisher.AudioTrack.FourCC() == codec.FourCC_MP4A {
return m7s.PlayBlock(s.Subscriber, s.sendADTS, s.sendVideo)
}
}
return m7s.PlayBlock(s.Subscriber, s.sendRawAudio, s.sendVideo)
return m7s.PlayBlock(s.Subscriber, s.sendADTS, s.sendVideo)
}
func (s *Sender) WritePESPacket(frame *mpegts.MpegtsPESFrame, pesPacket mpegts.MpegTsPESPacket) (err error) {