Some test text!
This JavaScript sample illustrates how WebViewer can be used to load videos and annotate frame by frame enhancing the collaboration aspect in a video workflow. To see an example, please visit our Video Demo. Learn more about our JavaScript PDF Library and Video guide.
Get StartedSamplesDownloadTo run this sample, get started with a free trial of PDFTron SDK.
JavaScript
HTML
/* eslint-disable */
const viewerElement = document.getElementById('viewer');
const DOCUMENT_ID = 'video';
const DEFAULT_ZOOM = 1.25;
WebViewer(
{
path: 'lib',
css: 'styles.css',
disabledElements: ['searchButton', 'pageNavOverlay', 'viewControlsButton', 'panToolButton'],
},
viewerElement
).then(async instance => {
instance.setTheme('dark');
const { docViewer, iframeWindow, setHeaderItems } = instance;
const annotManager = docViewer.getAnnotationManager();
const license = `---- Insert commercial license key here after purchase ----`;
// Extends document class to support documents of type 'video'
await WebViewer.Video.registerDocument(instance, license);
// Load a video at a specific url. This file needs to be relative to lib/ui/index.html.
const videoUrl = 'https://pdftron.s3.amazonaws.com/downloads/pl/video/video.mp4';
const thumbnail = 'https://pdftron.s3.amazonaws.com/downloads/pl/video/thumbnail.jpg';
WebViewer.Video.loadDocument(videoUrl, thumbnail);
// Add save annotations button
setHeaderItems(header => {
header.push({
type: 'actionButton',
img:
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>',
onClick: async () => {
// Save annotations when button is clicked
// widgets and links will remain in the document without changing so it isn't necessary to export them
// Make a POST request with XFDF string
const saveXfdfString = (documentId, xfdfString) => {
return new Promise(resolve => {
fetch(`/server/annotationHandler.js?documentId=${documentId}`, {
method: 'POST',
body: xfdfString,
}).then(response => {
if (response.status === 200) {
resolve();
}
});
});
};
const annotations = docViewer
.getDocument()
.getVideo()
.getAllAnnotations();
var xfdfString = await annotManager.exportAnnotations({ links: false, widgets: false, annotList: annotations });
await saveXfdfString(DOCUMENT_ID, xfdfString);
alert('Annotations saved successfully.');
},
});
});
// Load saved annotations
docViewer.on('documentLoaded', () => {
docViewer.zoomTo(DEFAULT_ZOOM);
const doc = docViewer.getDocument();
const video = docViewer.getDocument().getVideo();
// Make a GET request to get XFDF string
const loadXfdfString = documentId => {
return new Promise(resolve => {
fetch(`/server/annotationHandler.js?documentId=${documentId}`, {
method: 'GET',
}).then(response => {
if (response.status === 200) {
response.text().then(xfdfString => {
console.log(xfdfString);
resolve(xfdfString);
});
} else if (response.status === 204) {
console.warn(`Found no content in xfdf file /server/annotationHandler.js?documentId=${documentId}`);
resolve('');
} else {
console.warn(`Something went wrong trying to load xfdf file /server/annotationHandler.js?documentId=${documentId}`);
console.warn(`Response status ${response.status}`);
resolve('');
}
});
});
};
loadXfdfString(DOCUMENT_ID)
.then(xfdfString => {
const annotManager = docViewer.getAnnotationManager();
return annotManager.importAnnotations(xfdfString);
})
.then(() => {
video.updateAnnotationsToTime(0);
});
});
let once = false;
// Create the video UI controls
docViewer.on('pageComplete', (pageIndex, videoContainer) => {
if (once) return;
once = true;
const createElementFromHTML = htmlString => {
var div = document.createElement('div');
div.innerHTML = htmlString.trim();
return div.firstChild;
};
const doc = docViewer.getDocument();
const video = doc.getVideo();
const controls = createElementFromHTML(`
<div class="controls">
<div class="buttons-container">
<div class="video-buttons">
<svg id="play" class="icon" width="18px" height="18px" data-state="hidden">
<path d="M15.562 8.1L3.87.225c-.818-.562-1.87 0-1.87.9v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z"/>
</svg>
<svg id="pause" class="icon" width="18px" height="18px" data-state="hidden">
<path d="M6 1H3c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1zm6 0c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1h-3z"/>
</svg>
<svg id="volume" class="icon" width="20px" height="18px" data-state="visible">
<path d="M15.6 3.3c-.4-.4-1-.4-1.4 0-.4.4-.4 1 0 1.4C15.4 5.9 16 7.4 16 9c0 1.6-.6 3.1-1.8 4.3-.4.4-.4 1 0 1.4.2.2.5.3.7.3.3 0 .5-.1.7-.3C17.1 13.2 18 11.2 18 9s-.9-4.2-2.4-5.7z"/>
<path d="M11.282 5.282a.909.909 0 0 0 0 1.316c.735.735.995 1.458.995 2.402 0 .936-.425 1.917-.995 2.487a.909.909 0 0 0 0 1.316c.145.145.636.262 1.018.156a.725.725 0 0 0 .298-.156C13.773 11.733 14.13 10.16 14.13 9c0-.17-.002-.34-.011-.51-.053-.992-.319-2.005-1.522-3.208a.909.909 0 0 0-1.316 0zm-7.496.726H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z"/>
</svg>
<svg id="muted" class="icon" width="20px" height="18px" data-state="visible">
<path d="M12.4 12.5l2.1-2.1 2.1 2.1 1.4-1.4L15.9 9 18 6.9l-1.4-1.4-2.1 2.1-2.1-2.1L11 6.9 13.1 9 11 11.1zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z"/>
</svg>
</div>
<div class="video-buttons">
<span class="time" id="current-time">00:00:00</span>
<svg style="display: none;" class="icon" width="18px" height="18px">
<path d="M16.135 7.784a2 2 0 0 1-1.23-2.969c.322-.536.225-.998-.094-1.316l-.31-.31c-.318-.318-.78-.415-1.316-.094a2 2 0 0 1-2.969-1.23C10.065 1.258 9.669 1 9.219 1h-.438c-.45 0-.845.258-.997.865a2 2 0 0 1-2.969 1.23c-.536-.322-.999-.225-1.317.093l-.31.31c-.318.318-.415.781-.093 1.317a2 2 0 0 1-1.23 2.969C1.26 7.935 1 8.33 1 8.781v.438c0 .45.258.845.865.997a2 2 0 0 1 1.23 2.969c-.322.536-.225.998.094 1.316l.31.31c.319.319.782.415 1.316.094a2 2 0 0 1 2.969 1.23c.151.607.547.865.997.865h.438c.45 0 .845-.258.997-.865a2 2 0 0 1 2.969-1.23c.535.321.997.225 1.316-.094l.31-.31c.318-.318.415-.781.094-1.316a2 2 0 0 1 1.23-2.969c.607-.151.865-.547.865-.997v-.438c0-.451-.26-.846-.865-.997zM9 12a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/>
</svg>
</div>
</div>
<div class="timeline">
<div class="markers"></div>
<progress id="progress">
</progress>
<div class="tooltip" id="tooltip">
00:00:00
</div>
</div>
</div>
</div>
`);
videoContainer.appendChild(controls);
const play = controls.querySelector('#play');
const pause = controls.querySelector('#pause');
const volume = controls.querySelector('#volume');
const muted = controls.querySelector('#muted');
const progress = controls.querySelector('#progress');
const markers = controls.querySelector('.markers');
const currentTime = controls.querySelector('#current-time');
const tooltip = controls.querySelector('#tooltip');
const videoElement = video.getElement();
const showMarkers = annotations => {
while (markers.firstChild) {
markers.removeChild(markers.firstChild);
}
annotations.forEach(annotation => {
if (annotation.startTime <= video.duration) {
const marker = document.createElement('div');
marker.className = 'marker';
const percent = annotation.startTime / video.duration;
marker.onclick = () => {
goToTime(annotation.startTime);
};
const markerLeft = Math.floor(percent * progress.clientWidth) - 1;
marker.style = `left: ${markerLeft}px`;
markers.appendChild(marker);
}
});
};
annotManager.on('annotationChanged', () => {
showMarkers(video.getAllAnnotations());
});
showMarkers(video.getAllAnnotations());
play.setAttribute('data-state', 'visible');
pause.setAttribute('data-state', 'hidden');
videoElement.addEventListener('play', () => {
play.setAttribute('data-state', 'hidden');
pause.setAttribute('data-state', 'visible');
});
videoElement.addEventListener('pause', () => {
play.setAttribute('data-state', 'visible');
pause.setAttribute('data-state', 'hidden');
});
play.addEventListener('click', () => {
if (videoElement.paused || videoElement.ended) {
videoElement.play();
}
});
pause.addEventListener('click', () => {
videoElement.pause();
});
volume.setAttribute('data-state', 'visible');
muted.setAttribute('data-state', 'hidden');
videoElement.addEventListener('volumechange', e => {
if (videoElement.muted) {
volume.setAttribute('data-state', 'hidden');
muted.setAttribute('data-state', 'visible');
} else {
volume.setAttribute('data-state', 'visible');
muted.setAttribute('data-state', 'hidden');
}
});
volume.addEventListener('click', () => {
videoElement.muted = true;
});
muted.addEventListener('click', () => {
videoElement.muted = false;
});
progress.setAttribute('max', video.duration);
const timelineFrames = createElementFromHTML(`
<div id="timeline-frames" class="timeline-frames">
`);
let frameCanvases = {};
const onScroll = async () => {
const { x: timelineX, width: timelineWidth } = timelineFrames.getBoundingClientRect();
const innerFrames = Array.from(timelineFrames.getElementsByClassName('innerFrame')).filter(innerFrame => {
const { x: frameX, width: frameWidth } = innerFrame.getBoundingClientRect();
const isVisible = frameX + frameWidth > timelineX && frameX < timelineX + timelineWidth;
return innerFrame.children.length === 0 && isVisible;
});
const frameNumbers = innerFrames.map(innerFrame => innerFrame.frameNumber).filter(frameNumber => !frameCanvases[frameNumber]);
frameCanvases = { ...frameCanvases, ...(await video.extractFrames(frameNumbers)) };
innerFrames.forEach(innerFrame => {
const frame = frameCanvases[innerFrame.frameNumber];
if (frame) {
innerFrame.appendChild(frame);
} else {
console.warn(`Failed to get canvas for frame: ${innerFrame.frameNumber}`);
}
});
};
timelineFrames.addEventListener('scroll', _.debounce(onScroll, 250));
const marginRight = 8;
const widthFrameContainer = 92.5;
const timelineHeight = 99;
const timelineWidth = 770 / docViewer.getZoom();
const totalFrames = video.getTotalFrames();
let selectedFrameContainer;
HyperList.create(timelineFrames, {
height: timelineHeight,
width: timelineWidth,
horizontal: true,
itemHeight: marginRight + widthFrameContainer,
total: totalFrames,
generate(index) {
const frameNumber = index + 1;
const frameContainer = document.createElement('div');
frameContainer.id = `frame${frameNumber}`;
frameContainer.className = 'frame';
const innerFrameContainer = document.createElement('div');
innerFrameContainer.id = `innerFrame${frameNumber}`;
innerFrameContainer.frameNumber = frameNumber;
innerFrameContainer.className = 'innerFrame';
innerFrameContainer.style = `min-width: ${88}px; min-height: ${50}px;`;
frameContainer.onclick = () => {
goToTime(video.getTimeFromFrame(frameNumber));
};
const frameFooter = createElementFromHTML(`
<div class="frameFooter">
<div class="frameNumber">
<span>${video.getFormattedTime(video.getTimeFromFrame(frameNumber))}</span>
</div>
</div>
`);
// if (video.hasAnnotation(frameNumber)) {
// const pencilIcon = createElementFromHTML(`
// <svg width="18px" height="18px" viewBox="0 0 24 24">
// <use xlink:href="./plyr/plyr.svg#pencil"></use>
// </svg>
// `);
// frameFooter.insertBefore(pencilIcon, frameFooter.firstChild);
// }
const frame = frameCanvases[frameNumber];
if (frame && frame !== 'inprogress') {
innerFrameContainer.appendChild(frame);
}
frameContainer.appendChild(innerFrameContainer);
frameContainer.appendChild(frameFooter);
const currentFrameNumber = video.getFrameFromTime(videoElement.currentTime);
if (frameNumber === currentFrameNumber) {
frameContainer.setAttribute('data-state', 'selected');
selectedFrameContainer = frameContainer;
}
return frameContainer;
},
});
controls.appendChild(timelineFrames);
videoElement.addEventListener('timeupdate', async () => {
doc.getVideo().updateAnnotationsToTime(videoElement.currentTime);
if (selectedFrameContainer) {
selectedFrameContainer.removeAttribute('data-state');
}
const currentFrameNumber = video.getFrameFromTime(videoElement.currentTime);
selectedFrameContainer = iframeWindow.document.getElementById(`frame${currentFrameNumber}`);
if (selectedFrameContainer) {
selectedFrameContainer.setAttribute('data-state', 'selected');
}
});
const goToTime = time => {
videoElement.currentTime = time;
progress.value = videoElement.currentTime;
currentTime.innerHTML = `${video.getFormattedCurrentTime()}`;
const currentFrameNumber = video.getFrameFromTime(videoElement.currentTime);
const rightmostPosition = (currentFrameNumber - 1) * (marginRight + widthFrameContainer);
const scrollLeft = rightmostPosition - timelineFrames.clientWidth / 2 + widthFrameContainer / 2;
if (Math.abs(timelineFrames.scrollLeft - scrollLeft) < 800) {
timelineFrames.scroll({
left: scrollLeft,
behavior: 'smooth',
});
} else {
timelineFrames.scrollLeft = scrollLeft;
}
};
let stateCurrentTime;
progress.addEventListener('click', e => {
goToTime(stateCurrentTime);
});
tooltip.setAttribute('data-state', 'hidden');
progress.addEventListener('mousemove', e => {
const { width } = progress.getBoundingClientRect();
const percentage = (e.offsetX * docViewer.getZoom()) / width;
const newTime = percentage * progress.max;
stateCurrentTime = newTime;
tooltip.setAttribute('data-state', 'visible');
tooltip.style = `left: ${e.offsetX}px;`;
tooltip.innerHTML = `${video.getFormattedTime(newTime)}`;
});
progress.addEventListener('mouseout', e => {
tooltip.setAttribute('data-state', 'hidden');
});
onScroll();
});
});
Get unlimited trial usage of PDFTron SDK to bring accurate, reliable, and fast document processing capabilities to any application or workflow.
Select a platform to get started with your free trial.
Web
Android
iOS
Windows
Linux
Unlimited usage. No email address required.