PluginProbe
BeyondWords – AI audio for publishers / 7.2.0
BeyondWords – AI audio for publishers v7.2.0
7.2.0 7.1.0 trunk 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.1.0 4.1.1 4.1.2 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.3.0 4.4.0 4.5.0 4.5.1 4.6.0 4.6.1 4.6.2 All 44 releases
speechkit / src / editor / components / play-audio / hooks.js

hooks.js in BeyondWords – AI audio for publishers 7.2.0, at src/editor/components/play-audio/hooks.js

272 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* global HTMLScriptElement */
2
3 /**
4 * WordPress dependencies
5 */
6 import apiFetch from '@wordpress/api-fetch';
7 import { useSelect } from '@wordpress/data';
8 import { useEffect, useState } from '@wordpress/element';
9
10 /**
11 * Internal dependencies
12 */
13 import {
14 pollContentStatus,
15 PROCESSED_STATUS,
16 } from '../../lib/poll-content-status';
17
18 const PLAYER_SCRIPT_SRC =
19 'https://proxy.beyondwords.io/npm/@beyondwords/player@latest/dist/umd.js';
20
21 export function useBeyondWordsNamespace() {
22 const [ value, setValue ] = useState( () => {
23 return window?.BeyondWords ?? null;
24 } );
25
26 useEffect( () => {
27 // The script can finish loading between the useState initializer and this
28 // effect; re-read now so we don't listen on an already-loaded script.
29 if ( window?.BeyondWords ) {
30 setValue( window.BeyondWords );
31 return;
32 }
33
34 const onLoad = () => {
35 setValue( window?.BeyondWords ?? null );
36 };
37
38 const existingScript = document.head.querySelector(
39 `script[src="${ PLAYER_SCRIPT_SRC }"]`
40 );
41
42 if ( existingScript instanceof HTMLScriptElement ) {
43 existingScript.addEventListener( 'load', onLoad );
44 return () => {
45 existingScript.removeEventListener( 'load', onLoad );
46 };
47 }
48
49 const newScript = document.createElement( 'script' );
50
51 newScript.src = PLAYER_SCRIPT_SRC;
52 newScript.async = true;
53 newScript.defer = true;
54 newScript.addEventListener( 'load', onLoad );
55
56 document.head.appendChild( newScript );
57
58 return () => {
59 newScript.removeEventListener( 'load', onLoad );
60 };
61 }, [] );
62 return value;
63 }
64
65 /**
66 * Create a (preview) BeyondWords player once its content is ready.
67 *
68 * Always confirms `processed` before embedding a contentId, whether it just
69 * appeared this session or was already on the post when the editor opened —
70 * a just-published, still-processing post (e.g. a slower voice/model
71 * override) is otherwise indistinguishable from a long-finished one. Mirrors
72 * the classic editor metabox, which polls unconditionally for the same
73 * reason. See lib/poll-content-status.js.
74 *
75 * @param {Object} options Options.
76 * @param {HTMLElement} options.target Player mount node.
77 * @param {number} options.projectId BeyondWords project ID.
78 * @param {number} options.sourceId Post ID (client-side integration).
79 * @param {string} options.contentId BeyondWords content ID, if any.
80 * @param {string} options.previewToken Preview token, if any.
81 *
82 * @return {{player: (Object|null), status: (string|undefined), isPolling: boolean, timedOut: boolean}}
83 * The player instance and the current polling state.
84 */
85 export function useBeyondWordsPlayer( {
86 target,
87 projectId,
88 sourceId,
89 contentId,
90 previewToken,
91 } ) {
92 const BeyondWords = useBeyondWordsNamespace();
93
94 const [ player, setPlayer ] = useState( null );
95 const [ pollState, setPollState ] = useState( {
96 status: undefined,
97 isPolling: false,
98 timedOut: false,
99 } );
100
101 useEffect( () => {
102 if ( ! BeyondWords?.Player || ! target ) {
103 setPlayer( null );
104 setPollState( {
105 status: undefined,
106 isPolling: false,
107 timedOut: false,
108 } );
109 return;
110 }
111
112 let newPlayer;
113 let cancelled = false;
114 const controller = new AbortController();
115
116 const initPlayer = () => {
117 try {
118 const params = {
119 target,
120 projectId,
121 sourceId,
122 contentId,
123 loadContentAs: [ 'article' ],
124 previewToken: previewToken || '',
125 analyticsConsent: 'none',
126 playerStyle: 'small',
127 widgetStyle: 'none',
128 introsOutros: [],
129 adverts: [],
130 };
131
132 if ( contentId ) {
133 delete params.sourceId;
134 }
135
136 newPlayer = new BeyondWords.Player( params );
137 } catch {
138 setPlayer( null );
139
140 // @todo display error notice in Wordpress admin.
141
142 return;
143 }
144
145 setPlayer( newPlayer );
146 };
147
148 if ( contentId ) {
149 // Poll until processed, then embed, so a 404 is never CDN-cached
150 // for content that's still processing.
151 setPollState( {
152 status: undefined,
153 isPolling: true,
154 timedOut: false,
155 } );
156
157 pollContentStatus( {
158 fetchStatus: async () => {
159 const data = await apiFetch( {
160 path: `/beyondwords/v1/projects/${ projectId }/content/${ contentId }`,
161 signal: controller.signal,
162 } );
163 return { status: data?.status };
164 },
165 onTick: ( status ) => {
166 if ( ! cancelled ) {
167 setPollState( {
168 status,
169 isPolling: true,
170 timedOut: false,
171 } );
172 }
173 },
174 isHidden: () => document.hidden,
175 signal: controller.signal,
176 } )
177 .then( ( { status, timedOut } ) => {
178 if ( cancelled ) {
179 return;
180 }
181 setPollState( { status, isPolling: false, timedOut } );
182 if ( ! timedOut && status === PROCESSED_STATUS ) {
183 initPlayer();
184 }
185 } )
186 .catch( () => {
187 // Aborted (unmount or dependency change) — nothing to do.
188 } );
189 } else {
190 // Client-side integration, keyed on sourceId: nothing to poll,
191 // embed now.
192 setPollState( {
193 status: undefined,
194 isPolling: false,
195 timedOut: false,
196 } );
197 initPlayer();
198 }
199
200 return () => {
201 cancelled = true;
202 controller.abort();
203 setPlayer( null );
204 if ( newPlayer ) {
205 newPlayer.destroy();
206 }
207 };
208 }, [
209 BeyondWords?.Player,
210 target,
211 projectId,
212 sourceId,
213 contentId,
214 previewToken,
215 ] );
216
217 return {
218 player,
219 status: pollState.status,
220 isPolling: pollState.isPolling,
221 timedOut: pollState.timedOut,
222 };
223 }
224
225 /**
226 * Whether the post has everything the BeyondWords player needs to load a preview.
227 *
228 * Pure selector (usable in `useSelect`/`withSelect`, unit-testable). Legacy
229 * `podcast_id` keys are recognised so upgraded posts still preview.
230 *
231 * @param {Function} select Redux-style select() from `@wordpress/data`.
232 *
233 * @return {boolean} True when the player can load.
234 */
235 export function selectHasPlayAudioAction( select ) {
236 const { getEditedPostAttribute } = select( 'core/editor' );
237
238 const status = getEditedPostAttribute( 'status' );
239 const projectId = getEditedPostAttribute( 'meta' ).beyondwords_project_id;
240 const integrationMethod =
241 getEditedPostAttribute( 'meta' ).beyondwords_integration_method;
242
243 const beyondwordsContentId =
244 getEditedPostAttribute( 'meta' ).beyondwords_content_id;
245 const beyondwordsPodcastId =
246 getEditedPostAttribute( 'meta' ).beyondwords_podcast_id;
247 const speechkitPodcastId =
248 getEditedPostAttribute( 'meta' ).speechkit_podcast_id;
249
250 const contentId =
251 beyondwordsContentId || beyondwordsPodcastId || speechkitPodcastId;
252
253 const isClientSide = integrationMethod === 'client-side';
254
255 const hasClientSideContent = isClientSide && projectId;
256
257 const hasRestApiContent = ! isClientSide && projectId && contentId;
258
259 return Boolean(
260 status !== 'pending' && ( hasClientSideContent || hasRestApiContent )
261 );
262 }
263
264 /**
265 * Hook wrapper around {@link selectHasPlayAudioAction} for function components.
266 *
267 * @return {boolean} True when the player can load.
268 */
269 export function useHasPlayAudioAction() {
270 return useSelect( selectHasPlayAudioAction, [] );
271 }
272