diff --git a/player/multiview/README.md b/player/multiview/README.md new file mode 100644 index 0000000..54af6f8 --- /dev/null +++ b/player/multiview/README.md @@ -0,0 +1,9 @@ +# Multiview Playback + +Achieve multiview playback through a customizable layout + +### Tags + + - multiview + - multiplayer + - layout diff --git a/player/multiview/css/style.css b/player/multiview/css/style.css new file mode 100644 index 0000000..52d7f4f --- /dev/null +++ b/player/multiview/css/style.css @@ -0,0 +1,185 @@ +@media (min-width: 992px) { + .player-col { + flex: 0 0 100%; + max-width: 100%; + } +} + +.bmpui-ui-controlbar { + z-index: 1002; + visibility: hidden; +} +.bmpui-ui-controlbar.active { + visibility: visible; +} + +#player-container { + position: relative; + background-color: black; + box-sizing: border-box; +} +#player-container:before { + display: block; + content: ''; + width: 100%; + padding-bottom: 56.25%; + box-sizing: border-box; +} +.grid { + display: grid; + align-content: center; + align-items: center; + position: absolute; + left: 0; + top: 0; + inset: 0; +} + +@media (min-width: 768px) { + .grid { + gap: 5px 10px; + /* compensate TV border width */ + inset: 15px; + } + #player-container { + /* compensate TV border width */ + padding: 15px; + } +} + +#controlbars-container { + position: relative; + color: #fff; + font-family: sans-serif; + font-size: 1em; +} + +#player { + margin: auto; +} + +.bitmovinplayer-container { + min-width: unset; + min-height: unset; +} + +.tile { + aspect-ratio: 16/9; + border-radius: 10px; + overflow: hidden; + position: relative; + border: 2px solid black; + box-sizing: border-box; +} +.tile.primary { + border-color: #006aed; +} +.tile img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 10px; +} + +/* Layouts for different tile counts */ +.grid.tile-count-1 { + grid-template-columns: 1fr; +} +.grid.tile-count-2 { + grid-template-columns: 1fr 1fr; +} +.grid.tile-count-3 { + grid-template-columns: 1fr 1fr 1fr; + grid-template-areas: 'a a b' 'a a c'; +} +.grid.tile-count-3 > .tile:nth-child(1) { + grid-area: a; +} +.grid.tile-count-3 > .tile:nth-child(2) { + grid-area: b; +} +.grid.tile-count-3 > .tile:nth-child(3) { + grid-area: c; +} +.grid.tile-count-4 { + grid-template-columns: 3fr 1fr; + grid-template-rows: 1fr 1fr 1fr; + grid-template-areas: + "a b" + "a c" + "a d"; +} +.grid.tile-count-4 > .tile:nth-child(1) { + grid-area: a; +} +.grid.tile-count-4 > .tile:nth-child(2) { + grid-area: b; +} +.grid.tile-count-4 > .tile:nth-child(3) { + grid-area: c; +} +.grid.tile-count-4 > .tile:nth-child(4) { + grid-area: d; +} + +.carousel-container { + border: solid 1px #CBE0ED; + border-radius: 4px; +} +.carousel-title { + padding: 10px 15px 0 15px; +} +.carousel { + display: flex; + overflow-x: auto; + gap: 10px; + padding: 15px; + max-width: 90vw; + min-height: 80px; + background-color: #fff; +} + +.item { + flex: 0 0 150px; + aspect-ratio: 16 / 9; + position: relative; + border-radius: 10px; + overflow: hidden; + scroll-snap-align: start; + cursor: pointer; + transition: transform 0.3s, box-shadow 0.3s; +} +.item img { + width: 100%; + height: 100%; + object-fit: cover; +} +.item:hover { + transform: scale(1.05); + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2); +} +.item.selected { + outline: 2px solid #006aed; +} +.item .checkmark { + position: absolute; + top: 5px; + right: 5px; + width: 18px; + height: 18px; + color: white; + background-color: #006aed; + border-radius: 50%; + font-size: 0.8em; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.2s; +} +.item.selected .checkmark { + opacity: 1; +} \ No newline at end of file diff --git a/player/multiview/icon.svg b/player/multiview/icon.svg new file mode 100644 index 0000000..08d76b4 --- /dev/null +++ b/player/multiview/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/player/multiview/index.html b/player/multiview/index.html new file mode 100644 index 0000000..bc7b263 --- /dev/null +++ b/player/multiview/index.html @@ -0,0 +1,49 @@ + + + + +
+

+ Multiview playback lets your viewers watch multiple video feeds or camera angles simultaneously, + giving them a more immersive viewing experience that can be customized to the event. + Multiview is great for sports broadcasting, concerts, and live events to enhance audience engagement. +

+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+

How Bitmovin’s Multiview expands your streaming potential

+ +
+
diff --git a/player/multiview/info.json b/player/multiview/info.json new file mode 100644 index 0000000..405e749 --- /dev/null +++ b/player/multiview/info.json @@ -0,0 +1,23 @@ +{ + "title": "Multiview Playback", + "description": "Achieve multiview playback through a customizable layout", + "long_description": "Give your audience the ability to never miss a play and experience every angle with Multiview Playback on the Bitmovin Player.", + "executable": { + "executable": true, + "indexfile": "index.html" + }, + "code": { + "show_code": false, + "language": "js", + "files": [] + }, + "metadata":{ + "title":"Multiview Demo | Bitmovin", + "description": "Achieve multiview playback through a customizable layout" + }, + "tags": [ + "multiview", + "multiplayer", + "layout" + ] +} \ No newline at end of file diff --git a/player/multiview/js/script.js b/player/multiview/js/script.js new file mode 100644 index 0000000..b4e0286 --- /dev/null +++ b/player/multiview/js/script.js @@ -0,0 +1,271 @@ +const sources = [ + { + hls: 'https://cdn.bitmovin.com/content/sports-mashup/sports-mashup-hls/m3u8/master.m3u8', + poster: 'https://cdn.bitmovin.com/content/sports-mashup/poster.jpg', + title: 'Bitmovin Sports Mashup', + }, + { + dash: 'https://cdn.bitmovin.com/content/assets/MI201109210084/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd', + poster: 'https://cdn.bitmovin.com/content/assets/poster/hd/RedBull.jpg', + title: 'Red Bull - Art of Motion', + }, + { + dash: 'https://cdn.bitmovin.com/content/assets/bbb/stream.mpd', + poster: 'https://cdn.bitmovin.com/content/assets/poster/hd/BigBuckBunny.jpg', + title: 'Big Buck Bunny', + }, + { + dash: 'https://cdn.bitmovin.com/content/assets/sintel/sintel.mpd', + poster: 'https://cdn.bitmovin.com/content/assets/sintel/poster.png', + title: 'Sintel', + }, +]; +const playerConfig = { + key: '29ba4a30-8b5e-4336-a7dd-c94ff3b25f30', + buffer: { + audio: { forwardduration: 12 }, + video: { forwardduration: 12 } + }, + playback: { + muted: true, + autoplay: true + }, + ui: { + disableAutoHideWhenHovered: true, + } +}; + +const activeSources = []; +const allPlayers = []; +const unusedPlayers = []; +let draggedElement = null; +let primarySource = null; + +const controlBarsContainer = document.getElementById('controlbars-container'); + +function populateCarousel() { + const carousel = document.getElementById('carousel'); + + for (let i = 0; i < sources.length; i++) { + const source = sources[i]; + + const img = document.createElement('img'); + img.src = source.poster; + img.alt = source.title; + + const checkmark = document.createElement('span'); + checkmark.className = 'checkmark'; + checkmark.textContent = '✔'; + + const item = document.createElement('div'); + item.className = 'item'; + item.id = `${i}`; + item.addEventListener('click', () => toggleCarouselItem(item)); + + item.appendChild(img); + item.appendChild(checkmark); + carousel.appendChild(item); + } +} + +function initializePlayers() { + document.getElementById('0').classList.add('selected'); + document.getElementById('1').classList.add('selected'); + + enableSource(sources[0]); + enableSource(sources[1]); + + updateGrid(); +} + +const toggleCarouselItem = (item) => { + item.classList.toggle('selected'); + + const sourceId = parseInt(item.id); + const source = sources[sourceId]; + const isSelected = item.classList.contains('selected'); + if (isSelected) { + enableSource(source); + } else { + disableSource(source); + } + + updateGrid(); +} + +function enableSource(source) { + activeSources.push(source); +} + +function disableSource(source) { + activeSources.splice(activeSources.indexOf(source), 1); + + if (primarySource === source) { + const newPrimaryPlayer = findPlayerForSource(activeSources[0]); + setPrimaryPlayer(newPrimaryPlayer, activeSources[0]); + } + const player = findPlayerForSource(source); + player?.unload().then(() => { + unusedPlayers.push(player); + }); +} + +function updateGrid() { + const grid = document.getElementById('grid'); + + // Clear existing tiles and classes + grid.innerHTML = ''; + grid.className = 'grid'; + + // Add tiles dynamically + for (let i = 0; i < activeSources.length; i++) { + const source = activeSources[i]; + const player = getPlayerInstance(playerConfig, source); + + const tile = player.getContainer(); + tile.title = source.title; + + if (primarySource === null) { + setPrimaryPlayer(player, source); + } + + grid.appendChild(tile); + } + + // Set grid layout using CSS classes + grid.classList.add(`tile-count-${activeSources.length}`); +} + +function getPlayerId(player) { + return allPlayers.indexOf(player); +} + +function getControlBar(player) { + const playerId = getPlayerId(player); + return controlBarsContainer.querySelector('.bmpui-ui-controlbar[data-player-id="' + playerId + '"]'); +} + +function getPlayerInstance(playerConfig, source) { + let player = findPlayerForSource(source); + if (player) { + // An active player instance already exists for this source + return player; + } + + player = unusedPlayers.shift(); + if (player) { + // Re-use one of the unused player instances + player.load(source); + return player; + } + + // Create a new player instance + const container = createPlayerTile(); + const newPlayer = new bitmovin.player.Player(container, playerConfig); + container.addEventListener('click', event => onTileClicked(container, newPlayer, event), true); + + newPlayer.load(source).then(() => { + // Move the control bar to the controlbars-container to be full-width + // This lets us simulate a common control bar for all players + + const controlBar = container.getElementsByClassName('bmpui-ui-controlbar')[0]; + controlBar.setAttribute('data-player-id', getPlayerId(newPlayer).toString()); + if (source === primarySource) { + controlBar.classList.add('active'); + } + + controlBarsContainer.appendChild(controlBar); + }); + allPlayers.push(newPlayer); + + return newPlayer; +} + +function findPlayerForSource(source) { + return allPlayers.find(player => player.getSource() === source); +} + +function setPrimaryPlayer(newPrimaryPlayer, newPrimarySource) { + if (newPrimarySource === primarySource) { + return; + } + + const container = document.getElementById('player-container'); + + // Remove active classes from the previous primary player + container.querySelector('.bmpui-ui-controlbar.active')?.classList.remove('active'); + container.querySelector('.tile.primary')?.classList.remove('primary'); + + if (newPrimaryPlayer == null) { + primarySource = null; + return; + } + + // Add active classes to the new primary player + newPrimaryPlayer.getContainer().classList.add('primary'); + getControlBar(newPrimaryPlayer)?.classList.add('active'); + + const previousPrimaryPlayer = findPlayerForSource(primarySource); + if (previousPrimaryPlayer) { + // Keep the volume state consistent across players + if (previousPrimaryPlayer.isMuted()) { + newPrimaryPlayer.mute(); + } else { + newPrimaryPlayer.unmute(); + } + const volume = previousPrimaryPlayer.getVolume(); + newPrimaryPlayer.setVolume(volume); + previousPrimaryPlayer.mute(); + } + + primarySource = newPrimarySource; +} + +function createPlayerTile() { + const tile = document.createElement('div'); + + tile.classList.add('tile'); + tile.id = 'player'; + tile.draggable = true; + + tile.addEventListener('dragstart', event => { + if (event.target.id === 'player') { + draggedElement = event.target; + } + }); + tile.addEventListener('dragover', (event) => { + event.preventDefault(); + }); + tile.addEventListener('drop', (event) => { + event.preventDefault(); + + const targetElement = event.target.closest('#player'); + if (draggedElement == null || targetElement == null || draggedElement == targetElement) { + return; + } + + // Swap sources + const targetIndex = activeSources.findIndex(config => config.title === targetElement.title); + const sourceIndex = activeSources.findIndex(config => config.title === draggedElement.title); + + const temp = activeSources[targetIndex]; + activeSources[targetIndex] = activeSources[sourceIndex]; + activeSources[sourceIndex] = temp; + + updateGrid(); + }); + + return tile; +} + +const onTileClicked = (tile, player, event) => { + if (!tile.classList.contains('primary')) { + // Prevent play/pause button from having an effect when the source is not the primary one (requires another click) + event.stopPropagation(); + setPrimaryPlayer(player, player.getSource()); + } +} + +populateCarousel(); + +initializePlayers();