labo-components
Version:
520 lines (461 loc) • 18.4 kB
JSX
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
};