From f99f5b49ea904974982514d822d05400f565a760 Mon Sep 17 00:00:00 2001 From: Adam Fox Date: Thu, 29 Jun 2023 09:26:10 -0700 Subject: [PATCH] July 2023 Release of the APL 2023.2 compliant APL Viewhost Web For more details on this release refer to CHANGELOG.md To learn about APL see: https://developer.amazon.com/docs/alexa-presentation-language/understand-apl.html --- CHANGELOG.md | 10 ++++ README.md | 10 ++-- js/apl-html/lib/dts/Context.d.ts | 2 - js/apl-html/src/APLRenderer.ts | 20 +++----- js/apl-html/src/components/EditText.ts | 5 +- js/apl-html/src/media/IMediaPlayerHandle.ts | 2 + js/apl-html/src/media/MediaEventProcessor.ts | 52 +++++++++++--------- js/apl-html/src/media/MediaEventSequencer.ts | 1 + js/apl-html/src/media/MediaPlayerHandle.ts | 7 +++ js/apl-html/src/media/Resource.ts | 1 + js/apl-html/src/media/audio/Id3Parser.ts | 5 +- js/apl-html/src/utils/AplVersionUtils.ts | 4 +- package.json | 2 +- scripts/fetch.js | 2 +- wasm/config.cmake | 13 ++++- wasm/include/wasm/context.h | 1 - wasm/include/wasm/mediaplayer.h | 1 + wasm/src/context.cpp | 10 ---- wasm/src/mediaplayer.cpp | 9 ++++ 19 files changed, 94 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fa2486..7df390d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog for apl-viewhost-web +## [2023.2] +This release adds support for version 2023.2 of the APL specification. Please also see APL Core Library for changes: [apl-core-library CHANGELOG](https://github.com/alexa/apl-core-library/blob/master/CHANGELOG.md) + +### Added +- Add support for the seekTo ControlMedia command + +### Changed +- Remove usage of APL Core Library's deprecated getTheme API +- Bug fixes + ## [2023.1] This release adds support for version 2023.1 of the APL specification. Please also see APL Core Library for changes: [apl-core-library CHANGELOG](https://github.com/alexa/apl-core-library/blob/master/CHANGELOG.md) diff --git a/README.md b/README.md index 57fe159..7a12569 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Alexa Presentation Language (APL) Viewhost Web

- - - - + + + +

## Introduction @@ -16,7 +16,7 @@ platform or framework for which the view host was designed by leveraging the fun ### Prerequisites -* [NodeJS](https://nodejs.org/en/) - version 16.x or higher +* [NodeJS](https://nodejs.org/en/) - version 14.x or higher * [cmake](https://cmake.org/install/) - the easiest way to install on Mac is using `brew install cmake` * [Yarn](https://yarnpkg.com/getting-started/install) diff --git a/js/apl-html/lib/dts/Context.d.ts b/js/apl-html/lib/dts/Context.d.ts index 432d1fa..02c5eb0 100644 --- a/js/apl-html/lib/dts/Context.d.ts +++ b/js/apl-html/lib/dts/Context.d.ts @@ -38,8 +38,6 @@ declare namespace APL { public topComponent(): APL.Component; - public getTheme(): string; - public getBackground(): APL.IBackground; public setBackground(background: APL.IBackground): void; diff --git a/js/apl-html/src/APLRenderer.ts b/js/apl-html/src/APLRenderer.ts index ebfa197..c543dfe 100644 --- a/js/apl-html/src/APLRenderer.ts +++ b/js/apl-html/src/APLRenderer.ts @@ -527,13 +527,7 @@ export default abstract class APLRenderer { }); } - let docTheme: string = this.context.getTheme(); - if (docTheme !== 'light' && docTheme !== 'dark') { - // treat themes other than dark and light as dark - docTheme = 'dark'; - } - - this.setBackground(docTheme); + this.setBackground(); // begin update loop this.requestId = requestAnimationFrame(this.update); @@ -593,15 +587,15 @@ export default abstract class APLRenderer { return Object.keys(this.componentMap).length; } - private setBackground(docTheme: string) { + private setBackground() { + // Setting backgroundColor to black to ensure the correct behaviour + // of a gradient containing an alpha channel component + + this.view.style.backgroundColor = 'black'; + const background = this.context.getBackground(); - const backgroundColors = { - dark: 'black', - light: 'white' - }; // Spec: If the background property is partially transparent // the default background color of the device will show through - this.view.style.backgroundColor = backgroundColors[docTheme]; this.view.style.backgroundImage = background.gradient ? getCssGradient(background.gradient, this.logger) : getCssPureColorGradient(background.color); diff --git a/js/apl-html/src/components/EditText.ts b/js/apl-html/src/components/EditText.ts index 158592f..243a8c5 100644 --- a/js/apl-html/src/components/EditText.ts +++ b/js/apl-html/src/components/EditText.ts @@ -263,10 +263,7 @@ export class EditText extends ActionableComponent { } private setInputText = async () => { - const text = await this.filterText(this.props[PropertyKey.kPropertyText]); - if (text.length > 0) { - this.inputElement.value = text; - } + this.inputElement.value = await this.filterText(this.props[PropertyKey.kPropertyText]); } public focus = () => { diff --git a/js/apl-html/src/media/IMediaPlayerHandle.ts b/js/apl-html/src/media/IMediaPlayerHandle.ts index 2749db3..071ddd7 100644 --- a/js/apl-html/src/media/IMediaPlayerHandle.ts +++ b/js/apl-html/src/media/IMediaPlayerHandle.ts @@ -16,6 +16,8 @@ export interface IMediaPlayerHandle extends IMediaEventListener { seek(offset: number): Promise; + seekTo(position: number): Promise; + play(waitForFinish: boolean): Promise; pause(): Promise; diff --git a/js/apl-html/src/media/MediaEventProcessor.ts b/js/apl-html/src/media/MediaEventProcessor.ts index b207ba2..f72558c 100644 --- a/js/apl-html/src/media/MediaEventProcessor.ts +++ b/js/apl-html/src/media/MediaEventProcessor.ts @@ -193,31 +193,15 @@ export function createMediaEventProcessor(mediaEventProcessorArgs: MediaEventPro }, async seek({ seekOffset, fromEvent }): Promise { await ensureLoaded.call(this, fromEvent); - const mediaResource: IMediaResource = this.playbackManager.getCurrent(); - const mediaOffsetMs: number = mediaResource.offset; const currentPlaybackPositionMs: number = toMillisecondsFromSeconds( this.player.getCurrentPlaybackPositionInSeconds() ); - const desiredPlaybackPositionMs: number = currentPlaybackPositionMs + seekOffset; - const videoDurationMs = toMillisecondsFromSeconds(this.player.getDurationInSeconds()); - const isNonDefaultDuration: boolean = mediaResource.duration > 0; - const isCurrentPositionOutOfBounds: boolean = - videoDurationMs <= desiredPlaybackPositionMs; - - if (isCurrentPositionOutOfBounds) { - // minus unit time otherwise will rollover to start - if (isNonDefaultDuration) { - this.player.setCurrentTimeInSeconds(mediaOffsetMs + - toSecondsFromMilliseconds(mediaResource.duration) - 0.001); - } else { - this.player.setCurrentTimeInSeconds(toSecondsFromMilliseconds(videoDurationMs) - 0.001); - } - } else if (desiredPlaybackPositionMs < mediaOffsetMs) { - this.player.setCurrentTimeInSeconds(toSecondsFromMilliseconds(mediaOffsetMs)); - } else { - this.player.setCurrentTimeInSeconds(toSecondsFromMilliseconds(desiredPlaybackPositionMs)); - } - + setPlayerPosition(this.player, this.playbackManager.getCurrent(), currentPlaybackPositionMs + seekOffset); + this.updateMediaState(fromEvent); + }, + async seekTo({ position, fromEvent }): Promise { + await ensureLoaded.call(this, fromEvent); + setPlayerPosition(this.player, this.playbackManager.getCurrent(), position); this.updateMediaState(fromEvent); }, async rewind({ fromEvent }): Promise { @@ -438,3 +422,27 @@ function ensureValidMediaState(mediaState: any): mediaState is APL.IMediaState { function isValidMediaStateValue(n: any): n is number { return !Number.isNaN(n) && n !== undefined; } + +function setPlayerPosition(player: any, mediaResource: IMediaResource, desiredPlaybackPositionMs: number) { + const mediaOffsetMs: number = mediaResource.offset; + const videoDurationMs = toMillisecondsFromSeconds(player.getDurationInSeconds()); + const providedVideoDurationMs = mediaResource.duration; + const isDurationProvided: boolean = mediaResource.duration !== 0; + + // minus unit time for EOF otherwise will rollover to start + const endOfFileMs = videoDurationMs - 1; + const endOfOffsetAndDurationMs = mediaOffsetMs + providedVideoDurationMs - 1; + + // Calculate the range of the clipped track based on `offset` and `duration` values + const trueStart = Math.max(0, mediaOffsetMs); + const trueEnd = (isDurationProvided) + ? Math.min(endOfFileMs, endOfOffsetAndDurationMs) + : endOfFileMs; + + player.setCurrentTimeInSeconds(toSecondsFromMilliseconds( + Math.min( + Math.max(desiredPlaybackPositionMs, trueStart), + trueEnd + ) + )); +} diff --git a/js/apl-html/src/media/MediaEventSequencer.ts b/js/apl-html/src/media/MediaEventSequencer.ts index 1faaedc..60a25b9 100644 --- a/js/apl-html/src/media/MediaEventSequencer.ts +++ b/js/apl-html/src/media/MediaEventSequencer.ts @@ -14,6 +14,7 @@ export enum VideoInterface { PAUSE = 'pause', STOP = 'stop', SEEK = 'seek', + SEEKTO = 'seekTo', REWIND = 'rewind', PREVIOUS = 'previous', NEXT = 'next', diff --git a/js/apl-html/src/media/MediaPlayerHandle.ts b/js/apl-html/src/media/MediaPlayerHandle.ts index c5c77d5..f410c6f 100644 --- a/js/apl-html/src/media/MediaPlayerHandle.ts +++ b/js/apl-html/src/media/MediaPlayerHandle.ts @@ -74,6 +74,13 @@ export class MediaPlayerHandle implements IMediaPlayerHandle, IMediaEventListene }); } + public async seekTo(position: number): Promise { + this.eventSequencer.enqueueForProcessing(VideoInterface.SEEKTO, { + position, + fromEvent: true + }); + } + public async play(waitForFinish: boolean): Promise { // Route through video component so can be override if (!this.videoComponent) { diff --git a/js/apl-html/src/media/Resource.ts b/js/apl-html/src/media/Resource.ts index a88d335..5b8afed 100644 --- a/js/apl-html/src/media/Resource.ts +++ b/js/apl-html/src/media/Resource.ts @@ -63,6 +63,7 @@ export enum ControlMediaCommandName { PREVIOUS = 'previous', REWIND = 'rewind', SEEK = 'seek', + SEEKTO = 'seekTo', SETTRACK = 'setTrack' } diff --git a/js/apl-html/src/media/audio/Id3Parser.ts b/js/apl-html/src/media/audio/Id3Parser.ts index 633a9ed..498e8c4 100644 --- a/js/apl-html/src/media/audio/Id3Parser.ts +++ b/js/apl-html/src/media/audio/Id3Parser.ts @@ -111,7 +111,10 @@ const parseFirstTXXXFrame = (buffer : Uint8Array, offset : number) : IBaseMarker // Slice selects from the start byte, and ends at HEADER_LENGTH + length - 1 // We need to skip past the header and frame length to get to the end const contents = buffer.slice(start, offset + HEADER_LENGTH + length - 1); - const data = String.fromCharCode.apply(null, contents); + + // TODO: After we upgrade to Typescript > 2.8.0, this can be changed to: new TextDecoder('utf-8') + const textDecoder = new (window as any).TextDecoder('utf-8'); + const data = textDecoder.decode(contents) return JSON.parse(data); }; diff --git a/js/apl-html/src/utils/AplVersionUtils.ts b/js/apl-html/src/utils/AplVersionUtils.ts index 93a91e8..8b8ad29 100644 --- a/js/apl-html/src/utils/AplVersionUtils.ts +++ b/js/apl-html/src/utils/AplVersionUtils.ts @@ -17,6 +17,7 @@ export const APL_1_9 = 9; export const APL_2022_1 = 10; export const APL_2022_2 = 11; export const APL_2023_1 = 12; +export const APL_2023_2 = 13; export const APL_LATEST = Number.MAX_VALUE; export interface AplVersionUtils { @@ -38,7 +39,8 @@ export function createAplVersionUtils(): AplVersionUtils { ['1.9', APL_1_9], ['2022.1', APL_2022_1], ['2022.2', APL_2022_2], - ['2023.1', APL_2023_1] + ['2023.1', APL_2023_1], + ['2023.2', APL_2023_2] ]); return { diff --git a/package.json b/package.json index 723f587..c5fde16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "apl-viewhost-web", - "version": "2023.1.0", + "version": "2023.2.0", "description": "This is a Web-assembly version (WASM) of apl viewhost web.", "license": "Apache 2.0", "repository": { diff --git a/scripts/fetch.js b/scripts/fetch.js index 5b69509..53133e6 100644 --- a/scripts/fetch.js +++ b/scripts/fetch.js @@ -3,7 +3,7 @@ const https = require('https'); const fs = require('fs'); -const artifactUrl = 'https://d1gkjrhppbyzyh.cloudfront.net/apl-viewhost-web/92777dcb-9ef0-4824-ba45-18b1505eb190/index.js'; +const artifactUrl = 'https://d1gkjrhppbyzyh.cloudfront.net/apl-viewhost-web/ed26327f-31c5-4296-8dee-2bc2d159b901/index.js'; const outputFilePath = 'index.js'; const outputFile = fs.createWriteStream(outputFilePath); diff --git a/wasm/config.cmake b/wasm/config.cmake index 42ddf84..32a66f6 100644 --- a/wasm/config.cmake +++ b/wasm/config.cmake @@ -44,5 +44,14 @@ if(WASM_PROFILING) endif() #set compiler flags -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS} --bind -O1") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS} --bind -O1") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS} --bind") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS} --bind") + +# Set optimization level from build type +if(CMAKE_BUILD_TYPE MATCHES DEBUG) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O1") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O1") +else() + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3") +endif() diff --git a/wasm/include/wasm/context.h b/wasm/include/wasm/context.h index 941600d..2f1634b 100644 --- a/wasm/include/wasm/context.h +++ b/wasm/include/wasm/context.h @@ -26,7 +26,6 @@ struct ContextMethods { static apl::RootContextPtr create(emscripten::val options, emscripten::val text, emscripten::val metrics, emscripten::val content, emscripten::val config, emscripten::val scalingOptions); static apl::ComponentPtr topComponent(const apl::RootContextPtr& context); - static std::string getTheme(const apl::RootContextPtr& context); static emscripten::val getBackground(const apl::RootContextPtr& context); static void setBackground(const apl::RootContextPtr& context, emscripten::val background); static std::string getDataSourceContext(const apl::RootContextPtr& context); diff --git a/wasm/include/wasm/mediaplayer.h b/wasm/include/wasm/mediaplayer.h index 41a2f20..5c6cb83 100644 --- a/wasm/include/wasm/mediaplayer.h +++ b/wasm/include/wasm/mediaplayer.h @@ -37,6 +37,7 @@ class MediaPlayer : public apl::MediaPlayer { void previous() override; void rewind() override; void seek(int offset) override; + void seekTo(int position) override; void setTrackIndex(int trackIndex) override; void setAudioTrack(apl::AudioTrack audioTrack) override; void setMute(bool mute) override; diff --git a/wasm/src/context.cpp b/wasm/src/context.cpp index b9eb684..753c4ed 100644 --- a/wasm/src/context.cpp +++ b/wasm/src/context.cpp @@ -33,15 +33,6 @@ ContextMethods::topComponent(const apl::RootContextPtr& context) { return top; } -std::string -ContextMethods::getTheme(const apl::RootContextPtr& context) { - std::string theme = ""; - if (context) { - theme = context->getTheme(); - } - return theme; -} - emscripten::val ContextMethods::getBackground(const apl::RootContextPtr& context) { return background; @@ -504,7 +495,6 @@ EMSCRIPTEN_BINDINGS(apl_wasm_context) { emscripten::class_("Context") .smart_ptr("ContextPtr") .function("topComponent", &internal::ContextMethods::topComponent) - .function("getTheme", &internal::ContextMethods::getTheme) .function("getBackground", &internal::ContextMethods::getBackground) .function("setBackground", &internal::ContextMethods::setBackground) .function("getDataSourceContext", &internal::ContextMethods::getDataSourceContext) diff --git a/wasm/src/mediaplayer.cpp b/wasm/src/mediaplayer.cpp index 25e385b..dba150a 100644 --- a/wasm/src/mediaplayer.cpp +++ b/wasm/src/mediaplayer.cpp @@ -167,6 +167,15 @@ MediaPlayer::seek(int offset) mPlayer.call("seek", offset); } +void +MediaPlayer::seekTo(int position) +{ + if (!isActive()) return; + resolveExistingAction(); + + mPlayer.call("seekTo", position); +} + void MediaPlayer::setTrackIndex(int trackIndex) {