UNPKG

labo-components

Version:
520 lines (461 loc) 18.4 kB
import React from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; import IDUtil from "../../util/IDUtil"; import TranscriptUtil from "../../util/TranscriptUtil"; import FlexPlayerUtil from "../../util/FlexPlayerUtil"; import AVPlayer from "./mediaColumn/AVPlayer"; import ImageViewer from "./mediaColumn/ImageViewer"; import PlaylistDropdown from "./mediaColumn/PlaylistDropdown"; import ColumnHeader from "./ColumnHeader"; import BaseColumn from "./BaseColumn"; import Info from "../shared/Info"; import TimelineView from "./mediaColumn/TimelineView"; import MediaEvents from "./_MediaEvents"; import Strings from "./_Strings"; import { AnnotationEvents } from "./AnnotationClient"; import { ResourceViewerContext } from "./ResourceViewerContext"; /* This component takes on 3 possible playlists (for video, audio & images). From that it generates" - a playlist for given media objects (segments will be added to the timeline; not part of the playlist) - viewer for the active media object - viewer - video: player + timeline - audio: player + timeline - image: image viewer? Playlists for testing http://0.0.0.0:5304/tool/resource-viewer?id=FLM77223&cid=eye-desmet-films&st=werk http://0.0.0.0:5304/tool/resource-viewer?id=482379@program&cid=nisv-catalogue-aggr&st=%22openbaar%20vervoer%22 http://0.0.0.0:5304/tool/resource-viewer?id=50263@program&cid=nisv-catalogue-aggr http://0.0.0.0:5304/tool/resource-viewer?id=5313446@program&cid=nisv-catalogue-aggr&st=aardkloot http://0.0.0.0:5304/tool/resource-viewer?id=49689@program&cid=nisv-catalogue-aggr-18-158 https://mediasuite.xlab.nl/tool/resource-viewer?id=5176026@program&cid=nisv-catalogue-aggr https://mediasuite-test.rdlabs.beeldengeluid.nl/tool/resource-viewer?id=5230684@program&cid=nisv-catalogue-aggr&st=wereld http://mediasuite.clariah.nl/tool/resource-viewer?id=498233@program&cid=nisv-catalogue-aggr&st=smaaknu */ export default class MediaColumn extends React.Component { static contextType = ResourceViewerContext; constructor(props) { super(props); this.state = { playerAPI: null, // called after an AVPlayer has loaded a new mediaObject (via onPlayerReady) duration: -1, showPlaylist: false, }; //this.imageLabelRef = React.createRef(); } componentDidMount = () => { this.context.annotationClient.events.bind( AnnotationEvents.ON_SET_SELECTION, this.onSetSelection ); }; componentWillUnmount = () => { this.context.mediaEvents.unbind( MediaEvents.SET_PLAYER_POS, this.onPlayerSeek ); this.context.annotationClient.events.unbind( AnnotationEvents.ON_SET_SELECTION, this.onSetSelection ); }; /* ----------------------------- COMPONENT INTERACTION FUNCTIONS ------------------------ */ setActiveMediaObject = async (mediaObject) => { //tell the context to update the active media object await this.context.setActiveMediaObject(mediaObject); //now notify whoever is interested that a new media object is active! this.context.mediaEvents.trigger( MediaEvents.ACTIVE_MEDIA_OBJECT, mediaObject ); //hide the playlist drop-down this.setState({ showPlaylist: false }); }; // Receives a playerAPI after the AVPlayer was loaded onPlayerReady = (playerAPI) => { const resourceStart = this.context.activeMediaObject && this.context.activeMediaObject.resourceStart ? this.context.activeMediaObject.resourceStart : 0; //TODO get the active transcript type? (a new context property for the resourceViewer.jsx?) const transcript = this.context.getActiveTranscript(); const transcriptFirstHit = transcript ? this.context.getFirstHitInTranscript( this.context.query?.term, transcript ) : -1; if (this.context.urlParams && this.context.urlParams.startTime) { playerAPI.playerAPI.currentTime = resourceStart + parseInt(this.context.urlParams.startTime); } else if(transcriptFirstHit !== -1) { playerAPI.playerAPI.currentTime = resourceStart + transcriptFirstHit; } else { playerAPI.playerAPI.currentTime = resourceStart; } this.setState({ playerAPI: playerAPI }, () => { // reset, and register the SET_PLAYER_POS event in the mediaEvents // so other components can directly control the player this.context.mediaEvents .reset(MediaEvents.SET_PLAYER_POS) .bind(MediaEvents.SET_PLAYER_POS, this.onPlayerSeek); this.context.setPlayerAPI(playerAPI); }); }; // Seek on player and timeline onPlayerSeek = (pos) => { if (!this.context.activeMediaObject || !this.state.playerAPI) { return; } this.onAirSeek(pos); }; onAirSeek = (pos) => { FlexPlayerUtil.seekRelativeToOnAir( this.state.playerAPI, pos, this.context.activeMediaObject ); }; //Here you can delegate time to other components via a ref (faster than using state) onGetPosition = (realPos) => { if (this.context.activeMediaObject) { // the relative position is used to shield off off-air content const relativePosition = FlexPlayerUtil.timeRelativeToOnAir( realPos, this.context.activeMediaObject ); // Trigger event this.context.mediaEvents.trigger( MediaEvents.PLAYER_POS, relativePosition ); // Trigger RAW event this.context.mediaEvents.trigger( MediaEvents.PLAYER_POS_RAW, realPos ); } }; onGetDuration = (onAirDuration) => this.setState({ duration: onAirDuration }); // whenever the player calculated a duration (required for drawing timeline) /* ----------------------------- PLAYLIST FUNCTIONS ------------------------ */ // Toggle playlist visiblity togglePlaylist = () => { this.setState({ showPlaylist: !this.state.showPlaylist }); }; onPlaylistNext = () => { this.navPlaylist(1); }; onPlaylistPrev = () => { this.navPlaylist(-1); }; // Navigate playlist navPlaylist = (diff) => { if (!this.context.activeMediaObject) { return; } const activeId = this.context.activeMediaObject.assetId; let activeIndex = 0; this.props.mediaObjects.some((mediaObject, index) => { if (mediaObject.assetId == activeId) { activeIndex = index; return true; } return false; }); // Use diff to set a new active MediaObject this.setActiveMediaObject( this.props.mediaObjects[ (activeIndex + diff + this.props.mediaObjects.length) % this.props.mediaObjects.length ], null ); }; /* ----------------------------- ANNOTATION CLIENT EVENTS ------------------------ */ onSetSelection = () => this.forceUpdate(); /* ----------------------------- RENDER FUNCTIONS ------------------------ */ //renders the playlist for the list of video/audio mediaObjects (test with: ?id=FLM77223&cid=eye-desmet-films) renderAVPlaylist = ( mediaObjects, transcripts, //TODO make sure to think about what to do with non ASR transcripts initialSearchTerm, activeId ) => { if ( !mediaObjects || mediaObjects.length === 0 || (mediaObjects.length === 1 && !mediaObjects[0].segments) ) return null; return ( <PlaylistDropdown mediaObjects={mediaObjects} transcriptMatches={TranscriptUtil.calcTranscriptMatchesPerMediaObject( mediaObjects, transcripts, initialSearchTerm )} activeId={activeId} onSelect={this.setActiveMediaObject} /> ); }; renderImagePlaylist = (mediaObjects, initialSearchTerm) => console.debug("Implement this for flipping through images"); renderAVPlayer = (activeMediaObject, collectionConfig) => { return ( <AVPlayer mediaObject={activeMediaObject} useCredentials={activeMediaObject.requiresPlayoutAccess} // whether the player needs to send a cookie or not hideOffAirContent={collectionConfig.hideOffAirContent()} // whether to hide start- & end offsets or not onPlayerReady={this.onPlayerReady} onGetPosition={this.onGetPosition} onGetDuration={this.onGetDuration} /> ); }; renderUnknownPlayer = (activeMediaObject) => { if (!activeMediaObject) { return ( <div className="error"> This resource does not have a media object <br /> </div> ); } else if (activeMediaObject.mimeType === "application/javascript") { return ( <div className="error"> Deze media kan i.v.m. beperkingen m.b.t. auteursrecht of het type content niet binnen de media suite worden afgespeeld{" "} <br /> <a href={activeMediaObject.url} target="_external_js"> Bekijk de media extern </a> </div> ); } else { return ( <iframe src={activeMediaObject.url} width="650" height="550" /> ); } }; //TODO this is currently being wired up renderImageViewer = (activeMediaObject, collectionConfig, mediaObjects) => { if (!activeMediaObject || !mediaObjects) return null; if (mediaObjects.findIndex((mo) => mo.cors === false) === -1) { //image viewer requires CORS return ( //<div><p className="bg__imv__overlay_static" ref={this.imageLabelRef} id="html-overlay">Image ID: {activeMediaObject.assetId}</p> <ImageViewer //imageLabelRef={this.imageLabelRef} mediaObjects={mediaObjects} //image viewer uses an internal playlist (for now) useCredentials={collectionConfig.requiresPlayoutAccess()} annotationClient={this.context.annotationClient} /> //</div> ); } else { //just return an image tag TODO support for playlist //return mediaObjects.map(mo => <img src={mo.url} />); return <img src={activeMediaObject.url} />; } }; // Get mediatype of mediaObject getMediaType(mediaObject) { return mediaObject && mediaObject.mimeType ? mediaObject.mimeType.split("/")[0] : ""; } // Renders a player depending on the given mediaobject renderPlayer = ( activeMediaObject, collectionConfig, mediaObjects = null ) => { const renderPlayerError = (msg) => <div className="error">{msg}</div>; if ( collectionConfig.getAnonymousUserRestrictions().prohibitPlayout === true && this.context.user.id === "ANONYMOUS" ) { return renderPlayerError( "To view the content within this collection, you are required to login" ); } let player = null; switch (this.getMediaType(activeMediaObject)) { case "video": player = this.renderAVPlayer( activeMediaObject, collectionConfig ); break; case "audio": player = this.renderAVPlayer( activeMediaObject, collectionConfig ); break; case "image": player = this.renderImageViewer( activeMediaObject, collectionConfig, mediaObjects ); break; case "application": player = this.renderUnknownPlayer(activeMediaObject); break; case "": player = renderPlayerError( "No Media Object available for this resource" ); break; case undefined: player = renderPlayerError( "No Media Object available for this resource" ); break; case null: player = renderPlayerError( "No Media Object available for this resource" ); break; default: player = renderPlayerError( "Error: Unknown media type: " + mediaType ); break; } return player; }; renderTimelineView = ( mediaObject, duration, activeAnnotationTypes, showAnnotationPopup ) => { if (!mediaObject || duration == -1) { return null; } return ( <TimelineView mediaObject={mediaObject} duration={duration} activeAnnotationTypes={activeAnnotationTypes} showAnnotationPopup={showAnnotationPopup} /> ); }; renderHeader(togglePlaylist, activeAssetId, down, onNext, onPrev) { return ( <ColumnHeader> <div className="playlist"> {/* Playlist label */} <strong> <Info id="playlist_info" text={Strings.PLAYLIST_HELP} className="black left" /> {Strings.PLAYLIST_TITLE} </strong> {/* Active media object (selector) */} <span className={classNames("active", { down })} onClick={togglePlaylist} > {activeAssetId} </span> </div> {/* Playlist actions */} <div className="actions"> <button className="btn prev" title={Strings.PLAYLIST_PREV_TITLE} onClick={onPrev} ></button> <button className="btn next" title={Strings.PLAYLIST_NEXT_TITLE} onClick={onNext} ></button> </div> </ColumnHeader> ); } render() { // Render player based on the selected media type // optionally add a class for locked content const player = this.context.activeMediaObject ? ( <div className={classNames("player", { locked: this.context.collectionConfig.requiresPlayoutAccess() && !this.context.activeMediaObject.playoutAccess, })} > {this.renderPlayer( this.context.activeMediaObject, this.context.collectionConfig, this.props.mediaObjects )} </div> ) : null; // Render the timeline const timeline = this.context.resource && ["video", "audio"].includes( this.getMediaType(this.context.activeMediaObject) ) ? this.renderTimelineView( this.context.activeMediaObject, this.state.duration, this.context.activeAnnotationTypes, this.props.showAnnotationPopup ) : null; // Render the playlist dropdown const playlist = this.state.showPlaylist ? this.renderAVPlaylist( this.props.mediaObjects, this.props.transcripts, this.context.query ? this.context.query.term : null, this.context.activeMediaObject ? this.context.activeMediaObject.assetId : "" ) : null; // Only show the header if there are multiple MediaObjects or Segments const header = this.props.mediaObjects.length > 1 ? this.renderHeader( this.togglePlaylist, this.context.activeMediaObject ? this.context.activeMediaObject.assetId : null, this.state.showPlaylist, this.onPlaylistNext, this.onPlaylistPrev ) : null; return ( <BaseColumn className={IDUtil.cssClassName("viewer-column")}> {header} <div className="column-content"> {/* Playlist */} {playlist} {/* Player */} {player} {/* Timeline */} {timeline} </div> </BaseColumn> ); } } MediaColumn.propTypes = { mediaObjects: PropTypes.array, // TODO further specify (arrayOf(MediaObject.getProptypes(false))) transcripts: PropTypes.object, // All of the resource's transcripts showAnnotationPopup: PropTypes.func.isRequired, // Show the annotation popup; called from the timeline };