visit
So far in this blog series, we've created our first live streaming channel, learned how to create a playback experience in the browser, and enhanced that experience with the Amazon Interactive Video Service (Amazon IVS) player SDK. In the next few posts, we're going to look at adding some interactivity to our playback experience.
Live streaming is clearly popular, but it's more than just the video that appeals to viewers. Interacting with the broadcaster and other viewers is a major reason so many people keep coming back to certain streams and streamers. Amazon IVS gives us a few pretty amazing features that enable interactivity, and one of the cooler elements that we can use to enhance our live stream is timed metadata (obligatory "").
<video id="video-player" controls autoplay playsinline></video>
const videoEl = document.getElementById('video-player');
const streamUrl = '//fcc3ddae59ed.us-west-2.playback.live-video.net/api/video/v1/us-west-2.893648527354.channel.xhP3ExfcX8ON.m3u8';
const ivsPlayer = IVSPlayer.create();
ivsPlayer.attachHTMLVideoElement(videoEl);
ivsPlayer.load(streamUrl);
ivsPlayer.play();
This stream is an example of a “live trivia” stream where the host reads a few trivia questions. A timed metadata event is embedded at the exact point in time that the host reads a trivia question. We can listen for, and respond to this event. If you remember from our last post, we can use addEventListener()
on the Amazon IVS player to listen for various events, and TEXT_METADATA_CUE
() is one of those events.
ivsPlayer.addEventListener(IVSPlayer.PlayerEventType.TEXT_METADATA_CUE, (e) => {
console.log(e);
});
If we play our stream, we can check the console after the host reads a question and observe the logged event. It should look similar to this:
{
"startTime": 61.04233333333333,
"endTime": 61.04233333333333,
"type": "TextMetadataCue",
"description": "",
"text": "{\"question\": \"From what language does the term R.S.V.P. originate from?\",\"answers\": [ \"Russian\", \"Italian\", \"French\", \"Portuguese\" ],\"correctIndex\": 2}",
"owner": "metadata.live-video.net"
}
The startTime
key represents the time since the stream started the event took place (61 seconds in the example above). The interesting bit we're after here is the text
key, which contains the metadata. Here, it's a JSON string, but it can be whatever you need it to be.
const metadata = JSON.parse(e.text);
The parsed metadata structure uses the following structure:
{
"question": "From what language does the term R.S.V.P. originate from?",
"answers": [
"Russian",
"Italian",
"French",
"Portuguese"
],
"correctIndex": 2
}
<div class="card mt-3">
<div class="card-header bg-dark text-white">
<h4>Question</h4>
</div>
<div class="card-body">
<div class="card-title">
<h4 id="question"></h4>
</div>
<div id="answerContainer">
<ul id="answers" class="list-group"></ul>
</div>
</div>
<div class="card-footer text-center">
<button class="btn btn-dark" id="check-answer-btn" disabled>Submit</button>
</div>
</div>
So we've got a container for our question, and the answers. We've also added a button to check the answer that is initially disabled
. Let's add some code inside of our event handler to store the correct answer in the global scope so that we can check the user's answer later on.
window.correctIndex = metadata.correctIndex;
Now we can enable the submit button, display the question, and clear out the answer container.
// enable the submit button
document.getElementById('check-answer-btn').removeAttribute('disabled');
// display the question
document.getElementById('question').innerHTML = metadata.question;
// clear previous answers
const answersEl = document.getElementById('answers');
answersEl.replaceChildren();
Now we can loop over all the answers in the metadata object and render them to the page.
// render the answers for this question
metadata.answers.forEach((a, i) => {
// create an answer container
const answerEl = document.createElement('li');
answerEl.classList.add('list-group-item');
// radio button to select the answer
const answerRadio = document.createElement('input');
answerRadio.setAttribute('name', 'answer-input');
answerRadio.type = 'radio';
answerRadio.id = `answer-input-${i}`;
answerRadio.classList.add('me-2', 'form-check-input');
answerRadio.dataset.index = i;
answerEl.appendChild(answerRadio);
// label to display the answer text
const answerLbl = document.createElement('label');
answerLbl.setAttribute('for', `answer-input-${i}`);
answerLbl.innerHTML = a;
answerEl.appendChild(answerLbl);
answersEl.appendChild(answerEl);
});
const checkAnswer = () => {
// disable the submit btn
document.getElementById('check-answer-btn').setAttribute('disabled', 'disabled');
// check the current answer
const selectedAnswer = document.querySelector('input[name="answer-input"]:checked');
const selectedIdx = parseInt(selectedAnswer.dataset.index);
// highlight the correct answer
document.querySelector(`input[data-index="${window.correctIndex}"]`).nextSibling.classList.add('text-success');
// if they're wrong, highlight the incorrect answer
if (selectedIdx !== window.correctIndex) {
selectedAnswer.nextSibling.classList.add('text-danger');
}
}
Don't forget to attach an event listener for the click
event on our Submit button. We'll add that in our init()
function.
document.getElementById('check-answer-btn').addEventListener('click', checkAnswer);
And we're ready to see it in action! If you're playing along at home, check your code against the code in the demo below. If not, check it out anyway to see the final solution. Give it a shot and see how many questions you can get right!
We've looked at how to consume metadata, but we've not yet seen how to produce it. No worries - it's not that hard to do! There are several ways to produce timed metadata, and all of them require the Channel ARN. You can get this value from the Amazon IVS Management Console in the channel details, or via the AWS CLI.
$ aws \
ivs \
list-channels \
--filter-by-name [YOUR CHANNEL NAME] \
--query "channels[0].arn" \
--output text
Need to Install the CLI? Check out the
$ aws \
ivs \
put-metadata \
--channel-arn [YOUR CHANNEL ARN] \
--metadata '{"test": true}'
.
import AWS from 'aws-sdk';
const Ivs = new AWS.IVS({ region: 'us-east-1', credentials });
const putMetadata = async (metadata) => {
const input = {
channelArn: '[YOUR CHANNEL ARN]',
metadata: JSON.stringify(metadata)
};
let output;
try {
output = await Ivs.putMetadata(input).promise();
}
catch (e) {
console.error(e);
if (e.name === 'ChannelNotBroadcasting') {
output = { offline: true };
}
else {
throw new Error(e);
}
}
return output;
}
.
import boto3
client = boto3.client('ivs')
response = client.put_metadata(
channelArn='[YOUR CHANNEL ARN]',
metadata='{"python": true}'
)
print(response)
.