Welcome back to this series where we're learning how to get started with live streaming in the cloud with Amazon Interactive Video Service (Amazon IVS). If it's your first time joining us, I encourage you to catch up by checking out the rest of the posts in the series! Previously , we looked at how to broadcast our live stream from third-party desktop software. in this series In today's post, we're going to switch gears and look at a different option - broadcasting our live stream directly from a browser! Why? Good question! There are tons of desktop options for live streaming, and many of them offer features like custom scenes, backgrounds, animated transitions, and more. But sometimes the business rules of our applications don't require all of those features. Sometimes you just need to live stream a camera and microphone - and maybe share your desktop display with your viewers. Desktop streaming solutions require the installation and knowledge of third-party software. That's not always desired (or even an option). For these reasons (among others), the Amazon IVS web broadcast SDK gives us the ability to stream to our channel directly from web browsers. Broadcasting from a browser doesn't mean you can't have the fancy backgrounds, overlays, animations, and transitions available in many desktop offerings. There are tons of things possible with modern HTML and JavaScript, and we'll look at some of those in a future post. Today, we'll focus on primary camera and microphone streaming. Collecting Our Stream Information Like many of the previous posts in this series, we're going to build a demo using CodePen. We'll need a few bits of information in order to use the web broadcast SDK. Using the Amazon IVS Management Console One way we can grab these bits is by logging into the Amazon IVS Management Console ( ) and selecting the channel that we are working with. In the channel details, scroll down to the section and copy the and . https://console.aws.amazon.com/ivs Stream configuration Ingest endpoint Stream key Using the AWS CLI If you’d rather collect your and via the CLI, you can do so with a few separate calls. First, we’ll need the channel . We can get this via a call to ( ) and using the channel as a filter. In this example, is my channel . Ingest endpoint Stream key ARN list-channels docs Name demo-channel Name $ CHANNEL_ARN=$(aws ivs list-channels --filter-by-name "demo-channel" --query "channels[0].arn" --output text) Now we can obtain the via a call to ( ). Ingest endpoint get-channel docs $ aws ivs get-channel --arn=$CHANNEL_ARN --query "channel.ingestEndpoint" --output text To get the , we first need to obtain its . We can get this via ( ). Stream key ARN list-stream-keys docs $ STREAM_KEY_ARN=$(aws ivs list-stream-keys --channel-arn=$CHANNEL_ARN --query "streamKeys[0].arn" --output text) Finally, we retrieve the value with ( ). Stream key get-stream-key docs $ aws ivs get-stream-key --arn=$STREAM_KEY_ARN --query "streamKey.value" --output text Using an AWS SDK If you want to use an SDK to retrieve your and , refer to the for your favorite language. Here's an example of how you could accomplish this with the Node.JS SDK: Ingest endpoint Stream key SDK documentation import { IvsClient, GetChannelCommand, GetStreamKeyCommand, ListChannelsCommand, ListStreamKeysCommand } from "@aws-sdk/client-ivs"; const client = new IvsClient(); const channelName = 'demo-channel'; // list channels, filtering by name, to get the ARN const listChannelsRequest = new ListChannelsCommand({ filterByName: channelName }); const listChannelsResponse = await client.send(listChannelsRequest); if (!listChannelsResponse.channels.length) { console.warn(`No channels matching '${channelName}' were found!`); const process = await import('node:process') process.exit(1); } const channelArn = listChannelsResponse.channels[0].arn; console.log(`Channel ARN: ${channelArn}`); // get the channel (by ARN) to get ingestEndpoint const getChannelRequest = new GetChannelCommand({ arn: channelArn }); const getChannelResponse = await client.send(getChannelRequest); const ingestEndpoint = getChannelResponse.channel.ingestEndpoint; console.log(`Ingest Endpoint: ${ingestEndpoint}`); // list stream keys to get the stream key ARN const listStreamKeysRequest = new ListStreamKeysCommand({ channelArn: channelArn }); const listStreamKeysResponse = await client.send(listStreamKeysRequest); const streamKeyArn = listStreamKeysResponse.streamKeys[0].arn; // get stream key const getStreamKeyRequest = new GetStreamKeyCommand({ arn: streamKeyArn }); const getStreamKeyResponse = await client.send(getStreamKeyRequest); const streamKey = getStreamKeyResponse.streamKey.value; console.log(`Stream Key: ${streamKey}`); Running the code above with a valid would produce output similar to the following. channelName Channel ARN: arn:aws:ivs:us-east-1:<redacted>:channel/<redacted> Ingest Endpoint: <redacted>.global-contribute.live-video.net Stream Key: sk_us-east-1_<redacted> Building the Web Broadcast Demo To get started, we need to include the web broadcast SDK in our page. <script src="https://web-broadcast.live-video.net/1.1.0/amazon-ivs-web-broadcast.js"></script> Before we look at using the web broadcast SDK, let's add some HTML markup to the page. We'll start with a element that we can use for a live preview of what will be broadcasted to our viewers. <canvas> We'll add two elements to let the broadcaster select the camera and microphone used to capture audio and video. Since this demo is running in CodePen, we'll also add a few text inputs to capture the and that is required to configure the broadcast client. <select> Ingest endpoint Stream key We'll need a button to start the broadcast, so we'll add that below the text inputs. To handle layout and styling, I've included Bootstrap in the CodePen and applied some relevant classes to the layout and inputs. <div class="row"> <div class="col-sm-6 offset-sm-3"> <span class="badge bg-info fs-3 d-none mb-3 w-100" id="online-indicator">Online</span> <canvas id="broadcast-preview" class="rounded-4 shadow w-100"></canvas> </div> </div> <div class="d-flex flex-column col-sm-6 offset-sm-3 p-1"> <select name="cam-select" id="cam-select" class="form-select w-100 mb-3"></select> <select name="mic-select" id="mic-select" class="form-select w-100 mb-3"></select> <input type="text" name="endpoint" id="endpoint" class="form-control w-100 mb-3" placeholder="Ingest Endpoint" /> <input type="password" name="stream-key" id="stream-key" class="form-control w-100 mb-3" placeholder="Stream Key" /> <button class="btn btn-primary w-100 shadow" id="stream-btn">Stream</button> </div> If we run the demo, we can see the layout of the demo. Obviously, there won't be anything in the canvas preview or the select elements, because we haven't populated them yet. Wiring Up the Broadcast Preview, Cam, and Mic Let's wire up the demo with some JavaScript so that we can populate the camera and microphone drop-downs and preview the broadcaster's camera. Add an function and a handler to call this function when the DOM is ready. init() const init = async () => { }; document.addEventListener('DOMContentLoaded', init); For security, browsers won't let us access a user's cam and mic until we've asked for (and received) permission. Let's add a function to take care of this. handlePermissions() const handlePermissions = async () => { let permissions = { video: true, audio: true }; try { await navigator.mediaDevices.getUserMedia(permissions); } catch (err) { console.error(err.message); permissions = { video: false, audio: false }; } if (!permissions.video) console.error('Failed to get video permissions.'); if (!permissions.audio) console.error('Failed to get audio permissions.'); }; The function uses to obtain both and permissions via the object. We'll call this function the very first action inside of our function. handlePermissions() navigator.mediaDevices.getUserMedia() video audio permissions init() const init = async () => { await handlePermissions(); }; Next, we'll grab a list of video and audio (cam and mic) devices via and populate the elements. We'll set the first device that we find as the 'selected' device by default. navigator.mediaDevices.enumerateDevices() <select> const getDevices = async () => { const cameraSelect = document.getElementById('cam-select'); const micSelect = document.getElementById('mic-select'); const devices = await navigator.mediaDevices.enumerateDevices(); const videoDevices = devices.filter((d) => d.kind === 'videoinput'); const audioDevices = devices.filter((d) => d.kind === 'audioinput'); videoDevices.forEach((device, idx) => { const opt = document.createElement('option'); opt.value = device.deviceId; opt.innerHTML = device.label; if (idx === 0) { window.selectedVideoDeviceId = device.deviceId; opt.selected = true; } cameraSelect.appendChild(opt); }); audioDevices.forEach((device, idx) => { const opt = document.createElement('option'); opt.value = device.deviceId; opt.innerHTML = device.label; if (idx === 0) { window.selectedAudioDeviceId = device.deviceId; opt.selected = true; } micSelect.appendChild(opt); }); }; And update our function to call . init() getDevices() const init = async () => { await handlePermissions(); await getDevices(); }; Creating the Broadcast Client Now that we have permissions and have populated our available devices, we can . create an instance of the broadcast client const init = async () => { await handlePermissions(); await getDevices(); window.broadcastClient = IVSBroadcastClient.create({ streamConfig: IVSBroadcastClient.STANDARD_LANDSCAPE, }); }; Depending on the type of channel that you are broadcasting to, you may have to update the value to one of the available presets: streamConfig IVSBroadcastClient.BASIC_LANDSCAPE; IVSBroadcastClient.STANDARD_LANDSCAPE; IVSBroadcastClient.BASIC_PORTRAIT; IVSBroadcastClient.STANDARD_PORTRAIT; Creating the Video and Audio Streams Now that we have a broadcast client, we can via and respectively. We will reuse these functions to allow the broadcaster to switch their cam and mic mid-stream if they want to, so we'll add some logic to first remove any existing devices before we add the device. add our video and audio input devices to the client addVideoInputDevice() addAudioInputDevice() const createVideoStream = async () => { if (window.broadcastClient && window.broadcastClient.getVideoInputDevice('camera1')) window.broadcastClient.removeVideoInputDevice('camera1'); const streamConfig = IVSBroadcastClient.STANDARD_LANDSCAPE; window.videoStream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: { exact: window.selectedVideoDeviceId }, width: { ideal: streamConfig.maxResolution.width, max: streamConfig.maxResolution.width, }, height: { ideal: streamConfig.maxResolution.height, max: streamConfig.maxResolution.height, }, }, }); if (window.broadcastClient) window.broadcastClient.addVideoInputDevice(window.videoStream, 'camera1', { index: 0 }); }; The function is similar to the function: captureAudioStream() captureVideoStream() const createAudioStream = async () => { if (window.broadcastClient && window.broadcastClient.getAudioInputDevice('mic1')) window.broadcastClient.removeAudioInputDevice('mic1'); window.audioStream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: window.selectedAudioDeviceId }, }); if (window.broadcastClient) window.broadcastClient.addAudioInputDevice(window.audioStream, 'mic1'); }; Let's change the function to call these. init() const init = async () => { await handlePermissions(); await getDevices(); window.broadcastClient = IVSBroadcastClient.create({ streamConfig: IVSBroadcastClient.STANDARD_LANDSCAPE, }); await createVideoStream(); await createAudioStream(); }; Next, we'll add two functions to update the selected when the value changes. We'll add event listeners for these later on, so just add the functions for now. deviceId <select> const selectCamera = async (e) => { window.selectedVideoDeviceId = e.target.value; await createVideoStream(); }; const selectMic = async (e) => { window.selectedAudioDeviceId = e.target.value; await createAudioStream(); }; Previewing the Video Stream Now that we've added our video input device to the stream, we can in the element via a function. preview it <canvas> previewVideo() const previewVideo = () => { const previewEl = document.getElementById('broadcast-preview'); window.broadcastClient.attachPreview(previewEl); }; And add a call to to . previewVideo() init() const init = async () => { await handlePermissions(); await getDevices(); window.broadcastClient = IVSBroadcastClient.create({ streamConfig: IVSBroadcastClient.STANDARD_LANDSCAPE, }); await createVideoStream(); await createAudioStream(); previewVideo(); }; Getting Ready to Broadcast We're just about ready to broadcast to our channel. Let's add a function to handle clicks on the 'Stream' button. toggleBroadcast() const toggleBroadcast = () => { if(!window.isBroadcasting) { startBroadcast(); } else { stopBroadcast(); } }; Here we check to see if the stream is currently broadcasting and call or . startBroadcast() stopBroadcast() The function will check for the and , update the UI and then call on the to . startBroadcast() Ingest endpoint Stream key startBroadcast() broadcastClient begin the broadcast const startBroadcast = () => { const key = document.getElementById('stream-key').value; const endpoint = document.getElementById('endpoint').value; const streamBtn = document.getElementById('stream-btn'); const onlineIndicator = document.getElementById('online-indicator'); if(!key && !endpoint) { alert('Please enter a Stream Key and Ingest Endpoint!'); } else { window.broadcastClient .startBroadcast(key, endpoint) .then(() => { streamBtn.innerHTML = 'Stop'; onlineIndicator.classList.remove('d-none'); window.isBroadcasting = true; }) .catch((error) => { streamBtn.innerHTML = 'Stream'; onlineIndicator.classList.add('d-none'); window.isBroadcasting = false; console.error(error); }); } }; The function, as you might guess, will call on the and update the UI. stopBroadcast() stopBroadcast() broadcastClient const stopBroadcast = () => { window.broadcastClient.stopBroadcast(); window.isBroadcasting = false; document.getElementById('online-indicator').classList.add('d-none'); } Finally, we'll finish the function by adding event listeners to update the cam and mic devices, and call when the user clicks the 'Stream' button. init() toggleBroadcast() const init = async () => { await handlePermissions(); await getDevices(); window.broadcastClient = IVSBroadcastClient.create({ streamConfig: IVSBroadcastClient.STANDARD_LANDSCAPE, }); await createVideoStream(); await createAudioStream(); previewVideo(); document.getElementById('cam-select').addEventListener('change', selectCamera); document.getElementById('mic-select').addEventListener('change', selectMic); document.getElementById('stream-btn').addEventListener('click', toggleBroadcast); }; Broadcasting to an Amazon IVS Channel from the Web We're ready for broadcast! in a separate browser tab to get started. Open the demo Because CodePen runs embeds in an , we can't embed this demo directly into this blog post because of the sandboxed nature of the tag on HackerNoon. Please . Heads Up! <iframe> <iframe> view the CodePen directly in a separate browser tab Plug in your and and click 'Stream' to try it out. You can verify that your stream is broadcasting via the 'Live Preview' in the after the UI updates to confirm that your stream is online. Ingest endpoint Stream key Amazon IVS Management Console If your stream doesn't start broadcasting, verify your ingest endpoint and stream key are input exactly as shown in the Amazon IVS Management Console. If you're still having issues, check to see if a VPN connection is blocking the required port. If so, re-try the demo when disconnected from your VPN. Note: Summary In this post, we learned how to broadcast to our Amazon IVS channel via the Amazon IVS Web Broadcast SDK. For further reading, please refer to the . SDK docs Also Published . Here