visit
3 Amazon IVS Live Stream Playback with Chat Replay |
When trying to solve this problem, my first thought was to take a look at the metadata associated with a live stream to see if there is any valuable information hidden within it. Thankfully, there does appear to be a value in the regular stream of metadata that can be used for our chat playback purposes. In my testing, each stream contains ID3 metadata that appears to be injected by the Amazon IVS transcoding process. These ID3 tags contain a helpful timestamp that we can use to help with chat replay. To listen for these events, we can attach a handler that listens for the IVSPlayer.MetadataEventType.ID3
event type. This event type is documented, but the don't say much about it or make any guarantees about what it may contain.
Want to Avoid Undocumented Features? If you're concerned about using an undocumented feature, you could with the proper timestamp when new messages are posted into your Amazon IVS chat rooms. Keep in mind that there are
PutMetadata
events via the API.
Let's set up an Amazon IVS player to playback a recorded stream using the Player SDK. First, we'll include the latest Amazon IVS player SDK via a <script>
tag.
New to Amazon IVS? Check out the blog series . If you have questions on getting started, post a comment on any post in that series (or below)!
<script src="//player.live-video.net/1.16.0/amazon-ivs-player.min.js"></script>
As usual, we'll need to include a <video>
element in our HTML markup that will be used for playback.
<video id="video-player" muted controls autoplay playsinline></video>
const streamUrl = '//[redacted].cloudfront.net/ivs/v1/[redacted]/[redacted]/2022/11/17/18/6/[redacted]/media/hls/master.m3u8';
const videoEl = document.getElementById('video-player');
const ivsPlayer = IVSPlayer.create();
ivsPlayer.attachHTMLVideoElement(videoEl);
ivsPlayer.load(streamUrl);
ivsPlayer.play();
ivsPlayer.addEventListener(IVSPlayer.PlayerState.PLAYING, (evt) => {
window.time = Date.now();
});
ivsPlayer.addEventListener(IVSPlayer.MetadataEventType.ID3, (evt) => {
const now = Date.now();
console.log(`${(now - window.time) / 1000} seconds since last event`);
window.time = now;
});
window.ivsPlayer.addEventListener(IVSPlayer.MetadataEventType.ID3, (evt) => {
console.log(evt);
});
This is very interesting info, but a bit cryptic. Based on my testing, transc_s
seems to be timestamp that we're after. Let's modify the event handler to grab that timestamp and log it.
window.ivsPlayer.addEventListener(IVSPlayer.MetadataEventType.ID3, (evt) => {
const segmentMetadata = evt.find((tag) => tag.desc === 'segmentmetadata');
const segmentMetadataInfo = JSON.parse(segmentMetadata.info[0]);
const timestamp = segmentMetadataInfo['transc_s'];
const timestampWithMs = timestamp * 1000;
console.log(timestampWithMs);
console.log(new Date(timestamp));
});
When my page loads, I can utilize the method outlined in the previous post [todo: link] in this series to retrieve the entire chat log for the stream and render it in the chat container <div>
. Since no messages should be visible at the very start of the stream, I'll make sure that they call contain a class that hides them from the user and store a data attribute with the proper timestamp so that I can know which messages should be visible given any timestamp in the stream.
window.chatLog = await getChatLogs(logGroupName, chatArn, startTime, endTime);
renderChat();
My renderChat()
function handles posting each message to the chat container.
const renderChat = () => {
const chatContainer = document.getElementById('chat');
window.chatLog.forEach(msg => {
const msgTemplate = document.getElementById('chatMsgTemplate');
const msgEl = msgTemplate.content.cloneNode(true);
const ts = new Date(msg.event_timestamp).getTime() * 1000;
msgEl.querySelector('.msg-container').setAttribute('data-timestamp', ts);
msgEl.querySelector('.chat-username').innerHTML = msg.payload.Attributes.username;
msgEl.querySelector('.msg').innerHTML = msg.payload.Content;
chatContainer.appendChild(msgEl);
});
};
Now I can modify the ID3 listener to call a replayChat()
function and pass it the current timestamp.
window.ivsPlayer.addEventListener(IVSPlayer.MetadataEventType.ID3, (evt) => {
const segmentMetadata = evt.find((tag) => tag.desc === 'segmentmetadata');
const segmentMetadataInfo = JSON.parse(segmentMetadata.info[0]);
const timestamp = segmentMetadataInfo['transc_s'];
const timestampWithMs = timestamp * 1000;
replayChat(timestampWithMs);
});
In replayChat()
, I can find all of the chat nodes that contain a timestamp less than or equal to the current timestamp from the recorded stream and show/hide any chat message based on that timestamp.
const replayChat = (currentTimestamp) => {
Array.from(document.querySelectorAll('[data-timestamp]')).forEach(node => {
const chatMsgTs = Number(node.getAttribute('data-timestamp'));
const isVisible = chatMsgTs <= currentTimestamp;
if (isVisible) {
node.classList.remove('d-none');
}
else {
node.classList.add('d-none');
}
});
const chatContainer = document.getElementById('chat');
chatContainer.scrollTop = chatContainer.scrollHeight;
}