mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-06-24 17:05:47 +02:00
Add support for Screen annotations playing embedded media
Screen annotations whose rendition action resolves to an embedded audio/video file now play through the same play-button overlay as RichMedia. Factor the shared resolution logic into a MediaAnnotation base (used by both RichMedia and Screen). It fixes #6078 and #2787.
This commit is contained in:
parent
d71fe9025d
commit
d8ea2afe47
@ -19,6 +19,7 @@ import {
|
|||||||
AnnotationEditorType,
|
AnnotationEditorType,
|
||||||
AnnotationFieldFlag,
|
AnnotationFieldFlag,
|
||||||
AnnotationFlag,
|
AnnotationFlag,
|
||||||
|
AnnotationRenditionOperation,
|
||||||
AnnotationReplyType,
|
AnnotationReplyType,
|
||||||
AnnotationType,
|
AnnotationType,
|
||||||
assert,
|
assert,
|
||||||
@ -290,6 +291,9 @@ class AnnotationFactory {
|
|||||||
case "RichMedia":
|
case "RichMedia":
|
||||||
return new RichMediaAnnotation(parameters);
|
return new RichMediaAnnotation(parameters);
|
||||||
|
|
||||||
|
case "Screen":
|
||||||
|
return new ScreenAnnotation(parameters);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (!collectFields) {
|
if (!collectFields) {
|
||||||
if (!subtype) {
|
if (!subtype) {
|
||||||
@ -5457,13 +5461,124 @@ class FileAttachmentAnnotation extends MarkupAnnotation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RichMediaAnnotation extends Annotation {
|
/**
|
||||||
|
* Shared base for annotations that play an embedded audio/video clip:
|
||||||
|
* `RichMedia` (via `RichMediaContent`) and `Screen` (via a rendition action).
|
||||||
|
* Both resolve a single embedded media file and expose it identically through
|
||||||
|
* `data.richMedia`, so the display layer can render them with one element.
|
||||||
|
*/
|
||||||
|
class MediaAnnotation extends Annotation {
|
||||||
|
// The MIME types we can build a `<video>`/`<audio>` element for.
|
||||||
|
static #MEDIA_MIME_TYPE_RE = /^(?:video|audio)\//;
|
||||||
|
|
||||||
constructor(params) {
|
constructor(params) {
|
||||||
super(params);
|
super(params);
|
||||||
|
|
||||||
|
// No HTML element until a playable asset is found below by the subclass.
|
||||||
this.data.noHTML = true;
|
this.data.noHTML = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose a resolved embedded media asset as `data.richMedia`.
|
||||||
|
*
|
||||||
|
* @param {Object} asset
|
||||||
|
* @param {Ref | null} asset.assetRef
|
||||||
|
* Reference to the file-spec dictionary (or, for an inline file-spec, the
|
||||||
|
* embedded-file stream); used to lazily fetch the bytes on the main thread.
|
||||||
|
* @param {Dict} asset.assetDict
|
||||||
|
* The file-spec (or stream) dictionary, used to locate the embedded file
|
||||||
|
* when `assetRef` isn't itself a reference.
|
||||||
|
* @param {string} asset.filename
|
||||||
|
* @param {string} asset.contentType
|
||||||
|
* @param {Catalog} [catalog]
|
||||||
|
*/
|
||||||
|
_setMediaData({ assetRef, assetDict, filename, contentType }, catalog) {
|
||||||
|
let contentRef = assetRef;
|
||||||
|
if (!(contentRef instanceof Ref)) {
|
||||||
|
contentRef = FileSpec.pickPlatformItem(
|
||||||
|
assetDict.get("EF"),
|
||||||
|
/* raw = */ true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const fileId =
|
||||||
|
contentRef instanceof Ref
|
||||||
|
? catalog?.getAttachmentIdForAnnotation(contentRef)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
this.data.noHTML = false;
|
||||||
|
this.data.richMedia = { fileId, filename, contentType };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the MIME type used to build the `<video>`/`<audio>` element.
|
||||||
|
*
|
||||||
|
* When the media dictionary provides an explicit type (e.g. a MediaClip
|
||||||
|
* `/CT`), it's honored if it names an audio/video type. Otherwise, per the
|
||||||
|
* spec (ISO 32000-2, 7.11.4) an embedded file stream declares its MIME type
|
||||||
|
* through its own `/Subtype`, with characters not allowed in a name
|
||||||
|
* hex-escaped (e.g. `/video#2Fmp4` -> `video/mp4`). We trust that when it
|
||||||
|
* names an audio/video type, and otherwise fall back to mapping the filename
|
||||||
|
* extension. Returns `null` when the asset isn't a recognized audio/video
|
||||||
|
* type (e.g. Flash `.swf` or 3D models), so we don't build a player that
|
||||||
|
* can't play anything.
|
||||||
|
*
|
||||||
|
* @param {Dict} assetDict
|
||||||
|
* @param {string} filename
|
||||||
|
* @param {string | null} [contentType]
|
||||||
|
* @returns {string | null}
|
||||||
|
*/
|
||||||
|
static _getContentType(assetDict, filename, contentType = null) {
|
||||||
|
if (
|
||||||
|
typeof contentType === "string" &&
|
||||||
|
MediaAnnotation.#MEDIA_MIME_TYPE_RE.test(contentType)
|
||||||
|
) {
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The embedded file stream is keyed like a file-spec platform item.
|
||||||
|
const stream = FileSpec.pickPlatformItem(assetDict.get("EF"));
|
||||||
|
const subtype =
|
||||||
|
stream instanceof BaseStream ? stream.dict?.get("Subtype") : null;
|
||||||
|
if (
|
||||||
|
subtype instanceof Name &&
|
||||||
|
MediaAnnotation.#MEDIA_MIME_TYPE_RE.test(subtype.name)
|
||||||
|
) {
|
||||||
|
return subtype.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = filename.split(".").at(-1)?.toLowerCase();
|
||||||
|
switch (ext) {
|
||||||
|
case "mp4":
|
||||||
|
case "m4v":
|
||||||
|
return "video/mp4";
|
||||||
|
case "webm":
|
||||||
|
return "video/webm";
|
||||||
|
case "ogv":
|
||||||
|
return "video/ogg";
|
||||||
|
case "mov":
|
||||||
|
return "video/quicktime";
|
||||||
|
case "mp3":
|
||||||
|
return "audio/mpeg";
|
||||||
|
case "m4a":
|
||||||
|
return "audio/mp4";
|
||||||
|
case "wav":
|
||||||
|
return "audio/wav";
|
||||||
|
case "oga":
|
||||||
|
case "ogg":
|
||||||
|
return "audio/ogg";
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RichMediaAnnotation extends MediaAnnotation {
|
||||||
|
constructor(params) {
|
||||||
|
super(params);
|
||||||
|
|
||||||
const { dict, xref, annotationGlobals } = params;
|
const { dict, xref, annotationGlobals } = params;
|
||||||
|
/** @type {{catalog?: Catalog}} */
|
||||||
|
const { catalog } = annotationGlobals.pdfManager.pdfDocument;
|
||||||
|
|
||||||
const content = dict.get("RichMediaContent");
|
const content = dict.get("RichMediaContent");
|
||||||
if (!(content instanceof Dict)) {
|
if (!(content instanceof Dict)) {
|
||||||
@ -5475,24 +5590,8 @@ class RichMediaAnnotation extends Annotation {
|
|||||||
warn("RichMedia annotation has no playable asset.");
|
warn("RichMedia annotation has no playable asset.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { assetRef, assetDict, filename, contentType } = asset;
|
|
||||||
|
|
||||||
let contentRef = assetRef;
|
this._setMediaData(asset, catalog);
|
||||||
if (!(contentRef instanceof Ref)) {
|
|
||||||
contentRef = FileSpec.pickPlatformItem(
|
|
||||||
assetDict.get("EF"),
|
|
||||||
/* raw = */ true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const fileId =
|
|
||||||
contentRef instanceof Ref
|
|
||||||
? annotationGlobals.pdfManager.pdfDocument.catalog?.getAttachmentIdForAnnotation(
|
|
||||||
contentRef
|
|
||||||
)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
this.data.noHTML = false;
|
|
||||||
this.data.richMedia = { fileId, filename, contentType };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -5543,11 +5642,12 @@ class RichMediaAnnotation extends Annotation {
|
|||||||
if (!(asset instanceof Dict)) {
|
if (!(asset instanceof Dict)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Skip assets that only reference an external file we can't read.
|
||||||
|
if (!FileSpec.hasEmbeddedFile(asset)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const { filename } = new FileSpec(asset).serializable;
|
const { filename } = new FileSpec(asset).serializable;
|
||||||
const contentType = RichMediaAnnotation.#getContentType(
|
const contentType = MediaAnnotation._getContentType(asset, filename);
|
||||||
asset,
|
|
||||||
filename
|
|
||||||
);
|
|
||||||
if (!contentType) {
|
if (!contentType) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -5562,54 +5662,177 @@ class RichMediaAnnotation extends Annotation {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScreenAnnotation extends MediaAnnotation {
|
||||||
|
constructor(params) {
|
||||||
|
super(params);
|
||||||
|
|
||||||
|
const { dict, xref, annotationGlobals } = params;
|
||||||
|
const asset = ScreenAnnotation.#findAsset(dict, xref);
|
||||||
|
if (!asset) {
|
||||||
|
// Not every Screen annotation plays embedded media (e.g. a URL stream or
|
||||||
|
// a /Movie); such ones simply render their appearance, so don't warn.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._setMediaData(asset, annotationGlobals.pdfManager.pdfDocument.catalog);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the MIME type used to build the `<video>`/`<audio>` element.
|
* Locate the embedded media played by the annotation's rendition action.
|
||||||
*
|
*
|
||||||
* Per the spec (ISO 32000-2, 7.11.4) an embedded file stream declares its
|
* Per the spec (ISO 32000-1, 12.6.4.13 and 13.2) the chain is:
|
||||||
* MIME type through its own `/Subtype`, with characters not allowed in a
|
* Screen `/A` (or `/AA`) rendition action -> `/R` rendition (`/MR`)
|
||||||
* name hex-escaped (e.g. `/video#2Fmp4` -> `video/mp4`). We trust that when
|
* -> `/C` media clip (`/MCD`) -> `/D` file-spec -> `/EF` embedded file.
|
||||||
* it names an audio/video type, and otherwise fall back to mapping the
|
* Selector renditions (`/SR`) are unwrapped to their first playable media
|
||||||
* filename extension. Returns `null` when the asset isn't a recognized
|
* rendition. This mirrors `RichMediaAnnotation`, which also targets the
|
||||||
* audio/video type (e.g. Flash `.swf` or 3D models), so we don't build a
|
* common single embedded-media case.
|
||||||
* player that can't play anything.
|
|
||||||
*
|
*
|
||||||
* @param {Dict} assetDict
|
* @returns {{
|
||||||
* @param {string} filename
|
* assetRef: Ref | null,
|
||||||
* @returns {string | null}
|
* assetDict: Dict,
|
||||||
|
* filename: string,
|
||||||
|
* contentType: string,
|
||||||
|
* } | null}
|
||||||
*/
|
*/
|
||||||
static #getContentType(assetDict, filename) {
|
static #findAsset(dict, xref) {
|
||||||
// The embedded file stream is keyed like a file-spec platform item.
|
for (const action of this.#renditionActions(dict, xref)) {
|
||||||
const stream = FileSpec.pickPlatformItem(assetDict.get("EF"));
|
const asset = this.#findRenditionAsset(
|
||||||
const subtype =
|
action.get("R"),
|
||||||
stream instanceof BaseStream ? stream.dict?.get("Subtype") : null;
|
xref,
|
||||||
if (subtype instanceof Name && /^(?:video|audio)\//.test(subtype.name)) {
|
new RefSet()
|
||||||
return subtype.name;
|
);
|
||||||
|
if (asset) {
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static *#renditionActions(dict, xref) {
|
||||||
|
// The rendition action may be the activation action (/A) or one of the
|
||||||
|
// additional actions (/AA), e.g. page-open.
|
||||||
|
const action = xref.fetchIfRef(dict.getRaw("A"));
|
||||||
|
if (
|
||||||
|
action instanceof Dict &&
|
||||||
|
isName(action.get("S"), "Rendition") &&
|
||||||
|
this.#isPlayAction(action)
|
||||||
|
) {
|
||||||
|
yield action;
|
||||||
|
}
|
||||||
|
const additionalActions = dict.get("AA");
|
||||||
|
if (additionalActions instanceof Dict) {
|
||||||
|
for (const key of additionalActions.getKeys()) {
|
||||||
|
const aa = xref.fetchIfRef(additionalActions.getRaw(key));
|
||||||
|
if (
|
||||||
|
aa instanceof Dict &&
|
||||||
|
isName(aa.get("S"), "Rendition") &&
|
||||||
|
this.#isPlayAction(aa)
|
||||||
|
) {
|
||||||
|
yield aa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static #isPlayAction(action) {
|
||||||
|
// Rendition action /OP (ISO 32000-1, Table 214): PLAY_OR_RESUME and PLAY
|
||||||
|
// play; STOP/PAUSE/RESUME don't start playback. When absent, the action is
|
||||||
|
// JS-driven (/JS), which we can't run, so assume play.
|
||||||
|
const operation = action.get("OP");
|
||||||
|
return (
|
||||||
|
operation === undefined ||
|
||||||
|
operation === AnnotationRenditionOperation.PLAY_OR_RESUME ||
|
||||||
|
operation === AnnotationRenditionOperation.PLAY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static #findRenditionAsset(rendition, xref, seen) {
|
||||||
|
if (!(rendition instanceof Dict)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const subtype = rendition.get("S");
|
||||||
|
if (isName(subtype, "MR")) {
|
||||||
|
return this.#findClipAsset(rendition.get("C"), xref);
|
||||||
|
}
|
||||||
|
if (isName(subtype, "SR")) {
|
||||||
|
// A selector rendition lists candidate renditions; play the first that
|
||||||
|
// resolves to embedded media.
|
||||||
|
const renditions = rendition.get("R");
|
||||||
|
if (Array.isArray(renditions)) {
|
||||||
|
for (const ref of renditions) {
|
||||||
|
// Guard against renditions referencing each other in a cycle.
|
||||||
|
if (ref instanceof Ref) {
|
||||||
|
if (seen.has(ref)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.put(ref);
|
||||||
|
}
|
||||||
|
const asset = this.#findRenditionAsset(
|
||||||
|
xref.fetchIfRef(ref),
|
||||||
|
xref,
|
||||||
|
seen
|
||||||
|
);
|
||||||
|
if (asset) {
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static #findClipAsset(clip, xref) {
|
||||||
|
if (!(clip instanceof Dict) || !isName(clip.get("S"), "MCD")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rawData = clip.getRaw("D");
|
||||||
|
const data = xref.fetchIfRef(rawData);
|
||||||
|
const contentTypeHint = clip.get("CT");
|
||||||
|
let explicitType =
|
||||||
|
typeof contentTypeHint === "string" ? contentTypeHint : null;
|
||||||
|
|
||||||
|
let assetDict, filename;
|
||||||
|
if (data instanceof BaseStream) {
|
||||||
|
// `/D` is the embedded media stream directly.
|
||||||
|
assetDict = data.dict;
|
||||||
|
// `/N` is a human-readable label, not a filename, so it's an unreliable
|
||||||
|
// source for a file extension. When `/CT` is absent, prefer the stream's
|
||||||
|
// own `/Subtype` if it declares a media MIME type (as embedded-file
|
||||||
|
// streams do); `_getContentType` ignores a non-media value.
|
||||||
|
const name = clip.get("N");
|
||||||
|
filename = typeof name === "string" ? stringToPDFString(name) : "";
|
||||||
|
if (!explicitType) {
|
||||||
|
const subtype = data.dict.get("Subtype");
|
||||||
|
if (subtype instanceof Name) {
|
||||||
|
explicitType = subtype.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (data instanceof Dict) {
|
||||||
|
// `/D` is a file specification; require a readable embedded file.
|
||||||
|
if (!FileSpec.hasEmbeddedFile(data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
assetDict = data;
|
||||||
|
({ filename } = new FileSpec(data).serializable);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = filename.split(".").at(-1)?.toLowerCase();
|
const contentType = MediaAnnotation._getContentType(
|
||||||
switch (ext) {
|
assetDict,
|
||||||
case "mp4":
|
filename,
|
||||||
case "m4v":
|
explicitType
|
||||||
return "video/mp4";
|
);
|
||||||
case "webm":
|
if (!contentType) {
|
||||||
return "video/webm";
|
return null;
|
||||||
case "ogv":
|
|
||||||
return "video/ogg";
|
|
||||||
case "mov":
|
|
||||||
return "video/quicktime";
|
|
||||||
case "mp3":
|
|
||||||
return "audio/mpeg";
|
|
||||||
case "m4a":
|
|
||||||
return "audio/mp4";
|
|
||||||
case "wav":
|
|
||||||
return "audio/wav";
|
|
||||||
case "oga":
|
|
||||||
case "ogg":
|
|
||||||
return "audio/ogg";
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
assetRef: rawData instanceof Ref ? rawData : null,
|
||||||
|
assetDict,
|
||||||
|
filename,
|
||||||
|
contentType,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -107,6 +107,16 @@ class FileSpec {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a file specification carries an embedded file we can read.
|
||||||
|
*
|
||||||
|
* @param {Dict} fileSpecDict
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
static hasEmbeddedFile(fileSpecDict) {
|
||||||
|
return this.pickPlatformItem(fileSpecDict.get("EF")) instanceof BaseStream;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read attachment bytes from a file-spec dictionary.
|
* Read attachment bytes from a file-spec dictionary.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -158,8 +158,11 @@ class AnnotationElementFactory {
|
|||||||
case AnnotationType.FILEATTACHMENT:
|
case AnnotationType.FILEATTACHMENT:
|
||||||
return new FileAttachmentAnnotationElement(parameters);
|
return new FileAttachmentAnnotationElement(parameters);
|
||||||
|
|
||||||
|
// A Screen annotation with a rendition action plays embedded media the
|
||||||
|
// same way RichMedia does (see `MediaAnnotation` in the core layer).
|
||||||
case AnnotationType.RICHMEDIA:
|
case AnnotationType.RICHMEDIA:
|
||||||
return new RichMediaAnnotationElement(parameters);
|
case AnnotationType.SCREEN:
|
||||||
|
return new MediaAnnotationElement(parameters);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return new AnnotationElement(parameters);
|
return new AnnotationElement(parameters);
|
||||||
@ -403,7 +406,7 @@ class AnnotationElement {
|
|||||||
if (
|
if (
|
||||||
!(this instanceof WidgetAnnotationElement) &&
|
!(this instanceof WidgetAnnotationElement) &&
|
||||||
!(this instanceof LinkAnnotationElement) &&
|
!(this instanceof LinkAnnotationElement) &&
|
||||||
!(this instanceof RichMediaAnnotationElement)
|
!(this instanceof MediaAnnotationElement)
|
||||||
) {
|
) {
|
||||||
container.tabIndex = 0;
|
container.tabIndex = 0;
|
||||||
}
|
}
|
||||||
@ -3800,7 +3803,7 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RichMediaAnnotationElement extends AnnotationElement {
|
class MediaAnnotationElement extends AnnotationElement {
|
||||||
#abortController = new AbortController();
|
#abortController = new AbortController();
|
||||||
|
|
||||||
#contentUrl = null;
|
#contentUrl = null;
|
||||||
@ -3812,14 +3815,14 @@ class RichMediaAnnotationElement extends AnnotationElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.container.classList.add("richMediaAnnotation");
|
this.container.classList.add("mediaAnnotation");
|
||||||
|
|
||||||
const { filename } = this.data.richMedia;
|
const { filename } = this.data.richMedia;
|
||||||
|
|
||||||
// The annotation's appearance (a poster image) is painted on the canvas;
|
// The annotation's appearance (a poster image) is painted on the canvas;
|
||||||
// overlay a play button that loads the embedded media on demand.
|
// overlay a play button that loads the embedded media on demand.
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.className = "richMediaPlayButton";
|
button.className = "mediaPlayButton";
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.title = button.ariaLabel = filename;
|
button.title = button.ariaLabel = filename;
|
||||||
button.addEventListener("click", () => this.#load(button), {
|
button.addEventListener("click", () => this.#load(button), {
|
||||||
@ -3853,7 +3856,7 @@ class RichMediaAnnotationElement extends AnnotationElement {
|
|||||||
const isAudio = contentType.startsWith("audio/");
|
const isAudio = contentType.startsWith("audio/");
|
||||||
const media = document.createElement(isAudio ? "audio" : "video");
|
const media = document.createElement(isAudio ? "audio" : "video");
|
||||||
this.#media = media;
|
this.#media = media;
|
||||||
media.className = "richMediaContent";
|
media.className = "mediaContent";
|
||||||
this._setBackgroundColor(media);
|
this._setBackgroundColor(media);
|
||||||
media.src = url;
|
media.src = url;
|
||||||
media.title = filename;
|
media.title = filename;
|
||||||
|
|||||||
@ -175,6 +175,16 @@ const AnnotationReplyType = {
|
|||||||
REPLY: "R",
|
REPLY: "R",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Rendition action operations from Table 214, Section 12.6.4.13 of the PDF
|
||||||
|
// specification (ISO 32000-1).
|
||||||
|
const AnnotationRenditionOperation = {
|
||||||
|
PLAY_OR_RESUME: 0,
|
||||||
|
STOP: 1,
|
||||||
|
PAUSE: 2,
|
||||||
|
RESUME: 3,
|
||||||
|
PLAY: 4,
|
||||||
|
};
|
||||||
|
|
||||||
const AnnotationFlag = {
|
const AnnotationFlag = {
|
||||||
INVISIBLE: 0x01,
|
INVISIBLE: 0x01,
|
||||||
HIDDEN: 0x02,
|
HIDDEN: 0x02,
|
||||||
@ -1152,6 +1162,7 @@ export {
|
|||||||
AnnotationFlag,
|
AnnotationFlag,
|
||||||
AnnotationMode,
|
AnnotationMode,
|
||||||
AnnotationPrefix,
|
AnnotationPrefix,
|
||||||
|
AnnotationRenditionOperation,
|
||||||
AnnotationReplyType,
|
AnnotationReplyType,
|
||||||
AnnotationType,
|
AnnotationType,
|
||||||
assert,
|
assert,
|
||||||
|
|||||||
@ -987,8 +987,8 @@ describe("RichMedia annotation", () => {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
const annotationSelector = getAnnotationSelector("4R");
|
const annotationSelector = getAnnotationSelector("4R");
|
||||||
const buttonSelector = `${annotationSelector} .richMediaPlayButton`;
|
const buttonSelector = `${annotationSelector} .mediaPlayButton`;
|
||||||
const videoSelector = `${annotationSelector} video.richMediaContent`;
|
const videoSelector = `${annotationSelector} video.mediaContent`;
|
||||||
|
|
||||||
// Initially only the play button (over the poster) is shown.
|
// Initially only the play button (over the poster) is shown.
|
||||||
await page.waitForSelector(buttonSelector, { timeout: 0 });
|
await page.waitForSelector(buttonSelector, { timeout: 0 });
|
||||||
@ -1008,8 +1008,8 @@ describe("RichMedia annotation", () => {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
const annotationSelector = getAnnotationSelector("5R");
|
const annotationSelector = getAnnotationSelector("5R");
|
||||||
const buttonSelector = `${annotationSelector} .richMediaPlayButton`;
|
const buttonSelector = `${annotationSelector} .mediaPlayButton`;
|
||||||
const audioSelector = `${annotationSelector} audio.richMediaContent`;
|
const audioSelector = `${annotationSelector} audio.mediaContent`;
|
||||||
|
|
||||||
// Initially only the play button is shown.
|
// Initially only the play button is shown.
|
||||||
await page.waitForSelector(buttonSelector, { timeout: 0 });
|
await page.waitForSelector(buttonSelector, { timeout: 0 });
|
||||||
@ -1026,3 +1026,58 @@ describe("RichMedia annotation", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Screen annotation (rendition)", () => {
|
||||||
|
describe("multimedia_annotations.pdf", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"multimedia_annotations.pdf",
|
||||||
|
getAnnotationSelector("30R")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("must play the rendition video when clicking the play button", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
const annotationSelector = getAnnotationSelector("30R");
|
||||||
|
const buttonSelector = `${annotationSelector} .mediaPlayButton`;
|
||||||
|
const videoSelector = `${annotationSelector} video.mediaContent`;
|
||||||
|
|
||||||
|
await page.waitForSelector(buttonSelector, { visible: true });
|
||||||
|
await page.click(buttonSelector);
|
||||||
|
|
||||||
|
await page.waitForSelector(videoSelector, { visible: true });
|
||||||
|
const hasSource = await page.$eval(videoSelector, el =>
|
||||||
|
el.src.startsWith("blob:")
|
||||||
|
);
|
||||||
|
expect(hasSource).withContext(`In ${browserName}`).toEqual(true);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("must play the rendition audio when clicking the play button", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
const annotationSelector = getAnnotationSelector("6R");
|
||||||
|
const buttonSelector = `${annotationSelector} .mediaPlayButton`;
|
||||||
|
const audioSelector = `${annotationSelector} audio.mediaContent`;
|
||||||
|
|
||||||
|
await page.waitForSelector(buttonSelector, { visible: true });
|
||||||
|
await page.click(buttonSelector);
|
||||||
|
|
||||||
|
await page.waitForSelector(audioSelector, { visible: true });
|
||||||
|
const hasSource = await page.$eval(audioSelector, el =>
|
||||||
|
el.src.startsWith("blob:")
|
||||||
|
);
|
||||||
|
expect(hasSource).withContext(`In ${browserName}`).toEqual(true);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -214,7 +214,7 @@ describe("PDFPresentationMode", () => {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
const annotationSelector = getAnnotationSelector("4R");
|
const annotationSelector = getAnnotationSelector("4R");
|
||||||
const videoSelector = `${annotationSelector} video.richMediaContent`;
|
const videoSelector = `${annotationSelector} video.mediaContent`;
|
||||||
|
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
window.__richMediaNavigationCalls = 0;
|
window.__richMediaNavigationCalls = 0;
|
||||||
@ -229,11 +229,10 @@ describe("PDFPresentationMode", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await enterPresentationMode(page);
|
await enterPresentationMode(page);
|
||||||
await page.waitForSelector(
|
await page.waitForSelector(`${annotationSelector} .mediaPlayButton`, {
|
||||||
`${annotationSelector} .richMediaPlayButton`,
|
visible: true,
|
||||||
{ visible: true }
|
});
|
||||||
);
|
await page.click(`${annotationSelector} .mediaPlayButton`);
|
||||||
await page.click(`${annotationSelector} .richMediaPlayButton`);
|
|
||||||
await page.waitForSelector(videoSelector, { timeout: 0 });
|
await page.waitForSelector(videoSelector, { timeout: 0 });
|
||||||
|
|
||||||
const result = await page.$eval(videoSelector, el => ({
|
const result = await page.$eval(videoSelector, el => ({
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import {
|
|||||||
AnnotationEditorType,
|
AnnotationEditorType,
|
||||||
AnnotationFieldFlag,
|
AnnotationFieldFlag,
|
||||||
AnnotationFlag,
|
AnnotationFlag,
|
||||||
|
AnnotationRenditionOperation,
|
||||||
AnnotationType,
|
AnnotationType,
|
||||||
bytesToString,
|
bytesToString,
|
||||||
DrawOPS,
|
DrawOPS,
|
||||||
@ -145,6 +146,23 @@ describe("annotation", function () {
|
|||||||
partialEvaluator = null;
|
partialEvaluator = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function createAssetDict(filename, mimeSubtype = null) {
|
||||||
|
let streamDict = null;
|
||||||
|
if (mimeSubtype) {
|
||||||
|
streamDict = new Dict();
|
||||||
|
streamDict.set("Type", Name.get("EmbeddedFile"));
|
||||||
|
streamDict.set("Subtype", Name.get(mimeSubtype));
|
||||||
|
}
|
||||||
|
const embeddedFileDict = new Dict();
|
||||||
|
embeddedFileDict.set("F", new StringStream("", streamDict));
|
||||||
|
|
||||||
|
const fileSpecDict = new Dict();
|
||||||
|
fileSpecDict.set("Type", Name.get("Filespec"));
|
||||||
|
fileSpecDict.set("EF", embeddedFileDict);
|
||||||
|
fileSpecDict.set("UF", filename);
|
||||||
|
return fileSpecDict;
|
||||||
|
}
|
||||||
|
|
||||||
describe("AnnotationFactory", function () {
|
describe("AnnotationFactory", function () {
|
||||||
it("should get id for annotation", async function () {
|
it("should get id for annotation", async function () {
|
||||||
const annotationDict = new Dict();
|
const annotationDict = new Dict();
|
||||||
@ -4597,23 +4615,6 @@ describe("annotation", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("RichMediaAnnotation", function () {
|
describe("RichMediaAnnotation", function () {
|
||||||
function createAssetDict(filename, mimeSubtype = null) {
|
|
||||||
let streamDict = null;
|
|
||||||
if (mimeSubtype) {
|
|
||||||
streamDict = new Dict();
|
|
||||||
streamDict.set("Type", Name.get("EmbeddedFile"));
|
|
||||||
streamDict.set("Subtype", Name.get(mimeSubtype));
|
|
||||||
}
|
|
||||||
const embeddedFileDict = new Dict();
|
|
||||||
embeddedFileDict.set("F", new StringStream("", streamDict));
|
|
||||||
|
|
||||||
const fileSpecDict = new Dict();
|
|
||||||
fileSpecDict.set("Type", Name.get("Filespec"));
|
|
||||||
fileSpecDict.set("EF", embeddedFileDict);
|
|
||||||
fileSpecDict.set("UF", filename);
|
|
||||||
return fileSpecDict;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createAnnotation(contentDict, refNum) {
|
function createAnnotation(contentDict, refNum) {
|
||||||
const dict = new Dict();
|
const dict = new Dict();
|
||||||
dict.set("Type", Name.get("Annot"));
|
dict.set("Type", Name.get("Annot"));
|
||||||
@ -4905,6 +4906,336 @@ describe("annotation", function () {
|
|||||||
expect(data.noHTML).toEqual(true);
|
expect(data.noHTML).toEqual(true);
|
||||||
expect(data.richMedia).toBeUndefined();
|
expect(data.richMedia).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not create media data for an external (non-embedded) asset", async function () {
|
||||||
|
const assetRef = Ref.get(180, 0);
|
||||||
|
// A URL file specification with no `/EF`: the bytes aren't embedded, so
|
||||||
|
// there's nothing pdf.js can serve.
|
||||||
|
const assetDict = new Dict();
|
||||||
|
assetDict.set("Type", Name.get("Filespec"));
|
||||||
|
assetDict.set("FS", Name.get("URL"));
|
||||||
|
assetDict.set("F", "https://example.com/demo.mp4");
|
||||||
|
|
||||||
|
const instanceDict = new Dict();
|
||||||
|
instanceDict.set("Subtype", Name.get("Video"));
|
||||||
|
instanceDict.set("Asset", assetRef);
|
||||||
|
|
||||||
|
const configDict = new Dict();
|
||||||
|
configDict.set("Instances", [instanceDict]);
|
||||||
|
|
||||||
|
const contentDict = new Dict();
|
||||||
|
contentDict.set("Configurations", [configDict]);
|
||||||
|
|
||||||
|
const annotation = createAnnotation(contentDict, 181);
|
||||||
|
const xref = new XRefMock([
|
||||||
|
{ ref: assetRef, data: assetDict },
|
||||||
|
annotation,
|
||||||
|
]);
|
||||||
|
assetDict.assignXref(xref);
|
||||||
|
|
||||||
|
const { data } = await AnnotationFactory.create(
|
||||||
|
xref,
|
||||||
|
annotation.ref,
|
||||||
|
annotationGlobalsMock,
|
||||||
|
idFactoryMock
|
||||||
|
);
|
||||||
|
expect(data.annotationType).toEqual(AnnotationType.RICHMEDIA);
|
||||||
|
expect(data.noHTML).toEqual(true);
|
||||||
|
expect(data.richMedia).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ScreenAnnotation", function () {
|
||||||
|
function createMediaClipDict(fileSpecRef, contentType = null) {
|
||||||
|
const clipDict = new Dict();
|
||||||
|
clipDict.set("Type", Name.get("MediaClip"));
|
||||||
|
clipDict.set("S", Name.get("MCD"));
|
||||||
|
if (contentType) {
|
||||||
|
clipDict.set("CT", contentType);
|
||||||
|
}
|
||||||
|
clipDict.set("D", fileSpecRef);
|
||||||
|
return clipDict;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRenditionDict(clipDict) {
|
||||||
|
const renditionDict = new Dict();
|
||||||
|
renditionDict.set("Type", Name.get("Rendition"));
|
||||||
|
renditionDict.set("S", Name.get("MR"));
|
||||||
|
renditionDict.set("C", clipDict);
|
||||||
|
return renditionDict;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRenditionAction(
|
||||||
|
renditionDict,
|
||||||
|
refNum,
|
||||||
|
op = AnnotationRenditionOperation.PLAY_OR_RESUME
|
||||||
|
) {
|
||||||
|
const actionDict = new Dict();
|
||||||
|
actionDict.set("Type", Name.get("Action"));
|
||||||
|
actionDict.set("S", Name.get("Rendition"));
|
||||||
|
actionDict.set("OP", op);
|
||||||
|
actionDict.set("AN", Ref.get(refNum, 0));
|
||||||
|
actionDict.set("R", renditionDict);
|
||||||
|
return actionDict;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScreenAnnotation(refNum, { action = null, aa = null } = {}) {
|
||||||
|
const dict = new Dict();
|
||||||
|
dict.set("Type", Name.get("Annot"));
|
||||||
|
dict.set("Subtype", Name.get("Screen"));
|
||||||
|
if (action) {
|
||||||
|
dict.set("A", action);
|
||||||
|
}
|
||||||
|
if (aa) {
|
||||||
|
dict.set("AA", aa);
|
||||||
|
}
|
||||||
|
return { ref: Ref.get(refNum, 0), data: dict };
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should parse the media clip from a rendition action", async function () {
|
||||||
|
const fileSpecRef = Ref.get(200, 0);
|
||||||
|
const fileSpecDict = createAssetDict("demo.mp3");
|
||||||
|
const clipDict = createMediaClipDict(fileSpecRef, "audio/mpeg");
|
||||||
|
const rendition = createRenditionDict(clipDict);
|
||||||
|
const annotation = createScreenAnnotation(201, {
|
||||||
|
action: createRenditionAction(rendition, 201),
|
||||||
|
});
|
||||||
|
|
||||||
|
const xref = new XRefMock([
|
||||||
|
{ ref: fileSpecRef, data: fileSpecDict },
|
||||||
|
annotation,
|
||||||
|
]);
|
||||||
|
fileSpecDict.assignXref(xref);
|
||||||
|
|
||||||
|
const { data } = await AnnotationFactory.create(
|
||||||
|
xref,
|
||||||
|
annotation.ref,
|
||||||
|
annotationGlobalsMock,
|
||||||
|
idFactoryMock
|
||||||
|
);
|
||||||
|
expect(data.annotationType).toEqual(AnnotationType.SCREEN);
|
||||||
|
expect(data.noHTML).toEqual(false);
|
||||||
|
expect(data.richMedia).toEqual({
|
||||||
|
fileId: "attachmentRef:200R",
|
||||||
|
filename: "demo.mp3",
|
||||||
|
contentType: "audio/mpeg",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should derive the content type from the file extension", async function () {
|
||||||
|
const fileSpecRef = Ref.get(210, 0);
|
||||||
|
const fileSpecDict = createAssetDict("demo.mp4");
|
||||||
|
// No `/CT`, so the type is inferred from the filename extension.
|
||||||
|
const clipDict = createMediaClipDict(fileSpecRef);
|
||||||
|
const rendition = createRenditionDict(clipDict);
|
||||||
|
const annotation = createScreenAnnotation(211, {
|
||||||
|
action: createRenditionAction(rendition, 211),
|
||||||
|
});
|
||||||
|
|
||||||
|
const xref = new XRefMock([
|
||||||
|
{ ref: fileSpecRef, data: fileSpecDict },
|
||||||
|
annotation,
|
||||||
|
]);
|
||||||
|
fileSpecDict.assignXref(xref);
|
||||||
|
|
||||||
|
const { data } = await AnnotationFactory.create(
|
||||||
|
xref,
|
||||||
|
annotation.ref,
|
||||||
|
annotationGlobalsMock,
|
||||||
|
idFactoryMock
|
||||||
|
);
|
||||||
|
expect(data.richMedia).toEqual({
|
||||||
|
fileId: "attachmentRef:210R",
|
||||||
|
filename: "demo.mp4",
|
||||||
|
contentType: "video/mp4",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should derive the content type from a direct media stream's Subtype", async function () {
|
||||||
|
// `/D` is the embedded media stream itself (not a file spec), and `/N` is
|
||||||
|
// a display label with no extension, so the type comes from the stream's
|
||||||
|
// own `/Subtype`.
|
||||||
|
const streamRef = Ref.get(250, 0);
|
||||||
|
const streamDict = new Dict();
|
||||||
|
streamDict.set("Subtype", Name.get("video/mp4"));
|
||||||
|
const mediaStream = new StringStream("", streamDict);
|
||||||
|
|
||||||
|
const clipDict = new Dict();
|
||||||
|
clipDict.set("Type", Name.get("MediaClip"));
|
||||||
|
clipDict.set("S", Name.get("MCD"));
|
||||||
|
clipDict.set("N", "Intro clip");
|
||||||
|
clipDict.set("D", streamRef);
|
||||||
|
|
||||||
|
const rendition = createRenditionDict(clipDict);
|
||||||
|
const annotation = createScreenAnnotation(251, {
|
||||||
|
action: createRenditionAction(rendition, 251),
|
||||||
|
});
|
||||||
|
|
||||||
|
const xref = new XRefMock([
|
||||||
|
{ ref: streamRef, data: mediaStream },
|
||||||
|
annotation,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { data } = await AnnotationFactory.create(
|
||||||
|
xref,
|
||||||
|
annotation.ref,
|
||||||
|
annotationGlobalsMock,
|
||||||
|
idFactoryMock
|
||||||
|
);
|
||||||
|
expect(data.richMedia).toEqual({
|
||||||
|
fileId: "attachmentRef:250R",
|
||||||
|
filename: "Intro clip",
|
||||||
|
contentType: "video/mp4",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should unwrap a selector rendition to the first playable media", async function () {
|
||||||
|
// An empty media rendition (no clip) followed by a playable one.
|
||||||
|
const emptyRendition = new Dict();
|
||||||
|
emptyRendition.set("S", Name.get("MR"));
|
||||||
|
|
||||||
|
const fileSpecRef = Ref.get(220, 0);
|
||||||
|
const fileSpecDict = createAssetDict("demo.mp4");
|
||||||
|
const rendition = createRenditionDict(
|
||||||
|
createMediaClipDict(fileSpecRef, "video/mp4")
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectorDict = new Dict();
|
||||||
|
selectorDict.set("Type", Name.get("Rendition"));
|
||||||
|
selectorDict.set("S", Name.get("SR"));
|
||||||
|
selectorDict.set("R", [emptyRendition, rendition]);
|
||||||
|
|
||||||
|
const annotation = createScreenAnnotation(221, {
|
||||||
|
action: createRenditionAction(selectorDict, 221),
|
||||||
|
});
|
||||||
|
|
||||||
|
const xref = new XRefMock([
|
||||||
|
{ ref: fileSpecRef, data: fileSpecDict },
|
||||||
|
annotation,
|
||||||
|
]);
|
||||||
|
fileSpecDict.assignXref(xref);
|
||||||
|
|
||||||
|
const { data } = await AnnotationFactory.create(
|
||||||
|
xref,
|
||||||
|
annotation.ref,
|
||||||
|
annotationGlobalsMock,
|
||||||
|
idFactoryMock
|
||||||
|
);
|
||||||
|
expect(data.richMedia.filename).toEqual("demo.mp4");
|
||||||
|
expect(data.richMedia.contentType).toEqual("video/mp4");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should find a rendition action in the additional-actions dictionary", async function () {
|
||||||
|
const fileSpecRef = Ref.get(230, 0);
|
||||||
|
const fileSpecDict = createAssetDict("demo.mp4");
|
||||||
|
const rendition = createRenditionDict(
|
||||||
|
createMediaClipDict(fileSpecRef, "video/mp4")
|
||||||
|
);
|
||||||
|
|
||||||
|
// No `/A`; the rendition action is a page-visible additional action.
|
||||||
|
const aa = new Dict();
|
||||||
|
aa.set("PV", createRenditionAction(rendition, 231));
|
||||||
|
|
||||||
|
const annotation = createScreenAnnotation(231, { aa });
|
||||||
|
|
||||||
|
const xref = new XRefMock([
|
||||||
|
{ ref: fileSpecRef, data: fileSpecDict },
|
||||||
|
annotation,
|
||||||
|
]);
|
||||||
|
fileSpecDict.assignXref(xref);
|
||||||
|
|
||||||
|
const { data } = await AnnotationFactory.create(
|
||||||
|
xref,
|
||||||
|
annotation.ref,
|
||||||
|
annotationGlobalsMock,
|
||||||
|
idFactoryMock
|
||||||
|
);
|
||||||
|
expect(data.richMedia.filename).toEqual("demo.mp4");
|
||||||
|
expect(data.richMedia.contentType).toEqual("video/mp4");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore a non-play rendition action", async function () {
|
||||||
|
const fileSpecRef = Ref.get(235, 0);
|
||||||
|
const fileSpecDict = createAssetDict("demo.mp4");
|
||||||
|
const rendition = createRenditionDict(
|
||||||
|
createMediaClipDict(fileSpecRef, "video/mp4")
|
||||||
|
);
|
||||||
|
|
||||||
|
const annotation = createScreenAnnotation(236, {
|
||||||
|
action: createRenditionAction(
|
||||||
|
rendition,
|
||||||
|
236,
|
||||||
|
AnnotationRenditionOperation.PAUSE
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const xref = new XRefMock([
|
||||||
|
{ ref: fileSpecRef, data: fileSpecDict },
|
||||||
|
annotation,
|
||||||
|
]);
|
||||||
|
fileSpecDict.assignXref(xref);
|
||||||
|
|
||||||
|
const { data } = await AnnotationFactory.create(
|
||||||
|
xref,
|
||||||
|
annotation.ref,
|
||||||
|
annotationGlobalsMock,
|
||||||
|
idFactoryMock
|
||||||
|
);
|
||||||
|
expect(data.annotationType).toEqual(AnnotationType.SCREEN);
|
||||||
|
expect(data.noHTML).toEqual(true);
|
||||||
|
expect(data.richMedia).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create media data for an external file specification", async function () {
|
||||||
|
const fileSpecRef = Ref.get(237, 0);
|
||||||
|
const fileSpecDict = new Dict();
|
||||||
|
fileSpecDict.set("Type", Name.get("Filespec"));
|
||||||
|
fileSpecDict.set("FS", Name.get("URL"));
|
||||||
|
fileSpecDict.set("F", "https://example.com/demo.mp4");
|
||||||
|
|
||||||
|
const rendition = createRenditionDict(
|
||||||
|
createMediaClipDict(fileSpecRef, "video/mp4")
|
||||||
|
);
|
||||||
|
const annotation = createScreenAnnotation(238, {
|
||||||
|
action: createRenditionAction(rendition, 238),
|
||||||
|
});
|
||||||
|
|
||||||
|
const xref = new XRefMock([
|
||||||
|
{ ref: fileSpecRef, data: fileSpecDict },
|
||||||
|
annotation,
|
||||||
|
]);
|
||||||
|
fileSpecDict.assignXref(xref);
|
||||||
|
|
||||||
|
const { data } = await AnnotationFactory.create(
|
||||||
|
xref,
|
||||||
|
annotation.ref,
|
||||||
|
annotationGlobalsMock,
|
||||||
|
idFactoryMock
|
||||||
|
);
|
||||||
|
expect(data.annotationType).toEqual(AnnotationType.SCREEN);
|
||||||
|
expect(data.noHTML).toEqual(true);
|
||||||
|
expect(data.richMedia).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create media data without a rendition action", async function () {
|
||||||
|
// A URI action, not a rendition; the Screen carries no playable media.
|
||||||
|
const uriAction = new Dict();
|
||||||
|
uriAction.set("S", Name.get("URI"));
|
||||||
|
uriAction.set("URI", "https://example.com");
|
||||||
|
const annotation = createScreenAnnotation(241, { action: uriAction });
|
||||||
|
|
||||||
|
const xref = new XRefMock([annotation]);
|
||||||
|
|
||||||
|
const { data } = await AnnotationFactory.create(
|
||||||
|
xref,
|
||||||
|
annotation.ref,
|
||||||
|
annotationGlobalsMock,
|
||||||
|
idFactoryMock
|
||||||
|
);
|
||||||
|
expect(data.annotationType).toEqual(AnnotationType.SCREEN);
|
||||||
|
expect(data.noHTML).toEqual(true);
|
||||||
|
expect(data.richMedia).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("PopupAnnotation", function () {
|
describe("PopupAnnotation", function () {
|
||||||
|
|||||||
@ -328,19 +328,19 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.richMediaAnnotation {
|
.mediaAnnotation {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
|
||||||
.richMediaContent {
|
.mediaContent {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
video.richMediaContent {
|
video.mediaContent {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
audio.richMediaContent {
|
audio.mediaContent {
|
||||||
/* An `<audio>` element has a fixed-height control bar; pin it to the
|
/* An `<audio>` element has a fixed-height control bar; pin it to the
|
||||||
bottom of the section so it lines up with where `<video>` shows its
|
bottom of the section so it lines up with where `<video>` shows its
|
||||||
own bar. */
|
own bar. */
|
||||||
@ -348,7 +348,7 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.richMediaPlayButton {
|
.mediaPlayButton {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@ -285,7 +285,7 @@ class AnnotationLayerBuilder {
|
|||||||
for (const section of this.div.childNodes) {
|
for (const section of this.div.childNodes) {
|
||||||
if (
|
if (
|
||||||
section.hasAttribute("data-internal-link") ||
|
section.hasAttribute("data-internal-link") ||
|
||||||
section.classList.contains("richMediaAnnotation")
|
section.classList.contains("mediaAnnotation")
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -126,7 +126,7 @@ class PDFPresentationMode {
|
|||||||
if (!this.active) {
|
if (!this.active) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (evt.target.closest?.(".richMediaAnnotation")) {
|
if (evt.target.closest?.(".mediaAnnotation")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
@ -250,7 +250,7 @@ class PDFPresentationMode {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Allow interacting with embedded media controls rather than advancing.
|
// Allow interacting with embedded media controls rather than advancing.
|
||||||
if (evt.target.closest?.(".richMediaAnnotation")) {
|
if (evt.target.closest?.(".mediaAnnotation")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Unless an internal link was clicked, advance one page.
|
// Unless an internal link was clicked, advance one page.
|
||||||
@ -300,7 +300,7 @@ class PDFPresentationMode {
|
|||||||
if (!this.active) {
|
if (!this.active) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (evt.target.closest?.(".richMediaAnnotation")) {
|
if (evt.target.closest?.(".mediaAnnotation")) {
|
||||||
this.touchSwipeState = null;
|
this.touchSwipeState = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -237,7 +237,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pdfPresentationMode:fullscreen
|
.pdfPresentationMode:fullscreen
|
||||||
section:not([data-internal-link], .richMediaAnnotation) {
|
section:not([data-internal-link], .mediaAnnotation) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user