The video editing Web SDK demo covers basic features only. You can extend it with custom UI, AI capabilities, and media management to meet your needs.
Contents
Extended feature examples
To add features to the demo, modify fe/src/ProjectDetail.jsx. The following examples show common features you can add.
Get the SDK version number
Obtain the SDK version number dynamically at runtime.
window.AliyunVideoEditor.versionCustomize default subtitle
Override the default subtitle text ("online editing") with a custom string of up to 20 characters by passing defaultSubtitleText.
window.AliyunVideoEditor.init({
// Other parameters omitted.
defaultSubtitleText: 'Custom default text for subtitles'
})Custom button text
Use customTexts to customize the labels for the Import, Save, and Generate buttons.
window.AliyunVideoEditor.init({
// Other parameters are omitted.
customTexts: {
importButton: 'Custom text for Import',
updateButton: 'Custom text for Save',
produceButton: 'Custom text for Generate'
}
})Change the default aspect ratio
The default preview aspect ratio is 16:9. Specify defaultAspectRatio to change it. Supported values are listed in PlayerAspectRatio.
window.AliyunVideoEditor.init({
// Other parameters omitted.
defaultAspectRatio: '9:16'
})Retrieve timeline data
If you modify the returned timeline data, ensure it remains valid to prevent server API errors.
window.AliyunVideoEditor.getProjectTimeline()Custom back button
The Back button is hidden by default. Implement onBackButtonClick to display the Back button and customize its behavior.
window.AliyunVideoEditor.init({
// Other options omitted.
onBackButtonClick: () => {
window.location.href = '/mediaEdit/list'; // Redirects to another page, such as the project list page.
}
})Custom logo
No logo appears by default. Set customTexts.logoUrl to display one in the upper-left corner.
window.AliyunVideoEditor.init({
// Other parameters omitted.
customTexts: {
logoUrl: 'https://www.example.com/assets/example-logo-url.png'
}
})Customize media import
Implement searchMedia to handle the Import button. The function should search the media library, call AddEditingProjectMaterials to associate selected materials with the project, and return a Promise that resolves with the newly added materials. See fe/src/SearchMediaModal.jsx in the demo for a complete example.
Custom export
Implement produceEditingProjectVideo to handle the export video button. This function displays a configuration page where clicking Submit calls SubmitMediaProducingJob and returns a Promise that must be resolved. See fe/src/ProduceVideoModal.jsx in the demo for a complete example.
You can also use produceEditingProjectVideo to lock production parameters (such as destination bucket and format) or validate the timeline before export:
window.AliyunVideoEditor.init({
// Other options are omitted.
produceEditingProjectVideo: ({ timeline }) => { // Called when the user clicks the "Generate Video" button.
// Find all non-empty subtitle tracks.
const subtitleTracks = timeline.VideoTracks.filter((t) => t.Type === 'Subtitle' && t.VideoTrackClips.length > 0);
if (subtitleTracks.length < 2) {
// If there are fewer than two non-empty subtitle tracks, log an error and return.
console.error('The number of non-empty subtitle tracks is less than 2.');
return;
} else {
// At this point, send a video production request to the server (implementation omitted).
}
},
});Smart subtitle generation
The Intelligent Subtitling button is hidden by default. Pass AsrConfig to display it. See the demo code for the full implementation.
window.AliyunVideoEditor.init({
// Other options are omitted for clarity.
asrConfig: {
interval: 5000,
submitASRJob: async (mediaId, startTime, duration) => {
const res = await request("SubmitASRJob", {
InputFile: mediaId,
StartTime: startTime,
Duration: duration,
});
const jobId = get(res, "data.JobId");
return { jobId: jobId, jobDone: false };
},
getASRJobResult: async (jobId) => {
const res = await request("GetSmartHandleJob", {
JobId: jobId,
});
const isDone = get(res, "data.State") === "Finished";
const isError = get(res, "data.State") === "Failed";
let result;
if (res.data && res.data?.Output) {
result = JSON.parse(res.data?.Output);
}
return {
jobId,
jobDone: isDone,
result,
jobError: isError ? "Smart job failed" : undefined,
};
},
},
});Media tagging
Pass media asset marks
Pass media asset marks during import
In the
getEditingProjectMaterialsfunction, convert media assets from the OpenAPI format to the SDK format, including media marks.const markData = item.MediaDynamicInfo.DynamicMetaData.Data; if (markData) { const dataObject = JSON.parse(markData); marks = dataObject.MediaMark.map((m) => ({ startTime: m.MarkStartTime, endTime: m.MarkEndTime, content: m.MarkContent, })); result.video.marks = marks; }Pass media asset marks during task submission
Convert
mediaMarksto the OpenAPI format and pass them when calling SubmitMediaProducingJob.if (mediaMarks.length !== 0) { values.MediaMarks = mediaMarks.map((mark) => ({ MarkStartTime: mark.startTime, MarkEndTime: mark.endTime, MarkContent: mark.content, })); } const res = await request('SubmitMediaProducingJob', { ...values, });
Independently export each marked clip
Export each selected clip as an independent video. Click Export independently to open a dialog for configuring each video's name, storage location, format, resolution, and bitrate. You can reuse the Generate dialog.
window.AliyunVideoEditor.init({ ... exportFromMediaMarks: async (data) => { // Example of independently exporting marked clips. const projectId = ''; // Leave this as an empty string. A non-empty value might overwrite the timeline of the current project. // You can reuse the Generate dialog box logic to process these parameters and create the request parameters for the video production tasks. const reqParams = data.map((item, index) => { return { ProjectId: projectId, Timeline: JSON.stringify(item.timeline), OutputMediaTarget: 'oss-object', OutputMediaConfig: JSON.stringify({ // Set a custom filename. If exporting multiple files, you can include the index in the name for uniqueness. MediaURL: `https://example-bucket.oss-cn-shanghai.aliyuncs.com/example_${index}.mp4`, }), ....// Other custom parameters }; }); // Submit multiple video production tasks. await Promise.all( reqParams.map(async (params) => { // Use your custom request function to submit the video production task. request('SubmitMediaProducingJob',params) }), ); }, ... })
Split and export
Select multiple audio and video clips in the track area and click export as in the upper-right corner. The drop-down list provides these options:
Separate clip export
Exports selected clips as individual videos. Click Separate Clip Export to configure each video's name, storage location, format, resolution, and bitrate. You can reuse the Generate dialog.
window.AliyunVideoEditor.init({ ... exportVideoClipsSplit: async (data) => { // Example: separate clip export const projectId = ''; // Leave this parameter empty. A non-empty value will overwrite the timeline of the current project. // Reuse parameters from the Generate dialog box to build the request for the media production job. const reqParams = data.map((item, index) => { return { ProjectId: projectId, Timeline: JSON.stringify(item.timeline), OutputMediaTarget: 'oss-object', OutputMediaConfig: JSON.stringify({ // For multiple exports, use the index to generate unique filenames. MediaURL: `https://example-bucket.oss-cn-shanghai.aliyuncs.com/example_${index}.mp4`, }), ....//other business parameters }; }); // Submit multiple media production jobs. await Promise.all( reqParams.map(async (params) => { // Your API for submitting media production jobs. request('SubmitMediaProducingJob',params) }), ); }, ... })Composite clip export
Merges and exports selected clips as a single video. Click Composite Clip Export to submit a production job with default settings or configure video parameters. You can reuse the Generate dialog.
window.AliyunVideoEditor.init({ ... exportVideoClipsMerge: async (data) => { // Example: composite clip export const projectId = '';// Leave this parameter empty. A non-empty value will overwrite the timeline of the current project. // Reuse parameters from the Generate dialog box to build the request for the media production job. const reqParam = { ProjectId: projectId, Timeline: JSON.stringify(data.timeline), OutputMediaTarget: 'oss-object', OutputMediaConfig: JSON.stringify({ // Set a custom filename. MediaURL: 'https://example-bucket.oss-cn-shanghai.aliyuncs.com/example.mp4', }), }; // Your API for submitting media production jobs. await request('SubmitMediaProducingJob', reqParam); }, ... })Export video
Exports all timeline assets as a single video with layers, order, and effects preserved. Custom Export Interface.
AI dubbing
The Smart Dubbing button is hidden by default. Pass ttsConfig to display it. See the demo code for the full implementation.
window.AliyunVideoEditor.init({
// Other parameters are omitted.
ttsConfig: {
interval: 3000,
submitAudioProduceJob: async (text, voice, voiceConfig = {}) => {
const storageListReq = await requestGet("GetStorageList");
const tempFileStorageLocation =
storageListReq.data.StorageInfoList.find((item) => {
return item.EditingTempFileStorage;
});
if (!tempFileStorageLocation) {
throw new Error("The temporary storage path is not specified.");
}
const { StorageLocation, Path } = tempFileStorageLocation;
// The intelligent dubbing feature generates an audio file and saves it to your Object Storage Service (OSS) bucket. You can customize the example bucket, path, and filename below.
const bucket = StorageLocation.split(".")[0];
const path = Path;
const filename = `${text.slice(0, 10)}${Date.now()}`;
const editingConfig = voiceConfig.custom
? {
customizedVoice: voice,
format: "mp3",
...voiceConfig,
}
: {
voice,
format: "mp3",
...voiceConfig,
};
// 1. Submit an intelligent dubbing task.
const res1 = await request("SubmitAudioProduceJob", {
// https://www.alibabacloud.com/help/en/ims/developer-reference/api-ice-2020-11-09-submitaudioproducejob
EditingConfig: JSON.stringify(editingConfig),
InputConfig: text,
OutputConfig: JSON.stringify({
bucket,
object: `${path}${filename}`,
}),
});
if (res1.status !== 200) {
return { jobDone: false, jobError: "Could not recognize the provided text." };
} else {
const jobId = get(res1, 'data.JobId');
return { jobId: jobId, jobDone: false };
}
},
getAudioJobResult: async (jobId) => {
const res = await requestGet("GetSmartHandleJob",{
JobId: jobId,
});
const isJobDone = get(res, 'data.State') === 'Finished';
let isMediaReady = false;
let isError = get(res, 'data.State') === 'Failed';
let result;
let audioMedia;
let mediaId;
let asr = [];
if (res.data && res.data?.JobResult) {
try {
result = res.data.JobResult;
mediaId = result.MediaId;
if (result.AiResult) {
asr = JSON.parse(result.AiResult);
}
} catch (ex) {
console.error(ex);
}
}
if (!mediaId && res.data && res.data.Output) {
mediaId = res.data.Output;
}
const defaultErrorText = 'Could not recognize the provided text.';
if (mediaId) {
const mediaRes = await request("GetMediaInfo",{
MediaId: mediaId,
});
if (mediaRes.status !== 200) {
isError = true;
}
const mediaStatus = get(mediaRes, 'data.MediaInfo.MediaBasicInfo.Status');
if (mediaStatus === 'Normal') {
isMediaReady = true;
const transAudios = transMediaList([get(mediaRes, 'data.MediaInfo')]);
audioMedia = transAudios[0];
if (!audioMedia) {
isError = true;
}
} else if (mediaStatus && mediaStatus.indexOf('Fail') >= 0) {
isError = true;
}
} else if (isJobDone) {
isError = true;
}
return {
jobId,
jobDone: isJobDone && isMediaReady,
result: audioMedia,
asr,
jobError: isError ? defaultErrorText : undefined,
};
}
},
});Custom font list
The video editor supports these built-in Alibaba Cloud fonts:
// A list of officially supported fonts and their corresponding names.
const FONT_FAMILIES = [
'alibaba-sans', // Alibaba PuHuiTi
'fangsong', // Fangsong
'kaiti', // KaiTi
'SimSun', // SimSun
'siyuan-heiti', // Source Han Sans
'siyuan-songti', // Source Han Serif
'wqy-zenhei-mono', // WenQuanYi Zen Hei Mono
'wqy-zenhei-sharp', // WenQuanYi Zen Hei Sharp
'wqy-microhei', // WenQuanYi Micro Hei
'zcool-gaoduanhei', // ZCOOL GaoDuan Hei
'zcool-kuaile', // ZCOOL KuaiLe Ti
'zcool-wenyiti', // ZCOOL WenYi Ti
];To display a subset of official Alibaba Cloud fonts or to rearrange their order, pass an array of font names to the
customFontListparameter.window.AliyunVideoEditor.init({ // ... other options omitted customFontList: [ // Use only these fonts and display them in this order 'SimSun', 'kaiti', 'alibaba-sans', 'zcool-kuaile', 'wqy-microhei', ] });To use your custom fonts stored in an OSS bucket, add them to the
customFontListparameter.ImportantEnsure the account that submits a production task has permission to access the OSS bucket. Otherwise, the task will fail to download the font.
window.AliyunVideoEditor.init({ // ... other options omitted customFontList: [ // Use only these official fonts and your custom fonts 'SimSun', 'kaiti', 'alibaba-sans', 'zcool-kuaile', 'wqy-microhei', { key: 'OpenSansBold', // Required. A unique key for the font. Can be in Chinese or English. name: 'OpenSansBold', // The name displayed on the UI. // The URL of the font file. url: 'https://test-shanghai.oss-cn-shanghai.aliyuncs.com/xxxxx/OpenSansBold.ttf', }, { key: 'HussarBoldWeb', // Required. A unique key for the font. Can be in Chinese or English. name: 'HussarBoldWeb', // The name displayed on the UI. // The URL of the font file. url: 'https://test-shanghai.oss-cn-shanghai.aliyuncs.com/xxxxx/HussarBoldWeb.ttf', } ], /** * If your font URLs are dynamic, use the `getDynamicSrc` method to return the resolved URLs. * If you use `getDynamicSrc` for other media types, you must also handle the 'font' type. * * @param {string} mediaId The font's `key` from its object in the `customFontList` array, for example, `'HussarBoldWeb'`. * @param {string} mediaType The media type. For fonts, the value is `'font'`. * @param {string} mediaOrigin The media source, used to distinguish between public and private media. This is not used for fonts and will be `'undefined'`. * @param {string} InputURL The input URL of the font. * @returns {Promise<string>} A Promise that resolves to the final accessible URL of the file. */ getDynamicSrc: (mediaId, mediaType, mediaOrigin, InputURL) { // If the OSS bucket for the font is not dynamic, you can return the input URL directly. // if (mediaType === 'font') { // return Promise.resolve(InputURL); // } // Pseudocode for handling the 'font' media type if (mediaType === 'font') { return api.getFontUrl({ id: mediaId, url: InputURL }).then((res) => { return res.data.url; }); } // ... Code for handling other media types, such as video and audio, omitted for brevity. } });To use custom fonts stored in an IMS media asset library, specify them in the
customFontListparameter and implement thegetDynamicSrcmethod.ImportantEnsure the account that submits a production task has permission to access the IMS media asset library. Otherwise, the task will fail to download the font.
window.AliyunVideoEditor.init({ // ... other options are omitted getDynamicSrc: (mediaId, mediaType, mediaOrigin, InputURL) => { const params = { MediaId: mediaId, OutputType: 'cdn' }; // An example of dynamically retrieving a font URL from the media asset library by using InputURL. if (mediaType === 'font') { params.InputURL = InputURL; delete params.MediaId; } return request('GetMediaInfo', params).then((res) => { // Note: This is only an example. In practice, you should implement proper error handling to prevent exceptions, such as an error that occurs when FileInfoList is an empty array. return res.data.MediaInfo.FileInfoList[0].FileBasicInfo.FileUrl; }); }; });
Separate audio from video
To enable Separate audio track on the Basic tab, one of these conditions must be met:
If a media asset contains a proxy audio file, set the
hasTranscodedAudio=truetag. You can set this tag at two levels with different scopes and priorities:(Higher priority, recommended) Set the
Media.video.hasTranscodedAudio=truetag for a media asset when you import it into a project. This tag applies only to that specific media asset and indicates that it includes a pre-generated proxy audio file.(Lower priority) Make a global declaration by setting
config.hasTranscodedAudio=truewhen you initialize the video editor. This declaration indicates that all media assets in the project have proxy audio files. If a media asset does not have theMedia.video.hasTranscodedAudiotag, this global declaration applies, enabling audio track separation for the asset. Otherwise, theMedia.video.hasTranscodedAudiotag takes precedence.
The media asset contains an audio track and has an original video length of 30 minutes or less.
Generating proxy audio
If your media assets are stored in IMS, ApsaraVideo Media Processing (MPS) can transcode the audio. Once complete, a proxy audio file appears on the Video URL tab of the media asset details page.
You can transcode a video's audio by using one of the following methods:
On the Audio/Video page, click Media Processing in the Actions column, and then select an audio transcoding template or workflow.
On the Task Management page, create an audio transcoding task.
On the Upload Audio/Video page, set Media Processing to Media Processing after Upload and select an audio transcoding workflow.
How to tag a media asset with hasTranscodedAudio=true
When you import media assets, the searchMedia and getEditingProjectMaterials API operations must check for transcoded audio during data conversion.
// Note: The video editing SDK for web does not provide a request method. This is a reference sample. You can use a network request library, such as Axios.
window.AliyunVideoEditor.init({
...,
getEditingProjectMaterials: () => {
if (projectId) { // The projectId is saved by your application.
return request('GetEditingProjectMaterials', { // https://help.aliyun.com/document_detail/209068.html
ProjectId: projectId
}).then((res) => {
const data = res.data.MediaInfos;
return transMediaList(data); // Data conversion is required. For more information, see the code below.
});
}
return Promise.resolve([]);
},
...
});
/**
* Converts the media asset information from the server to the format required by the video editing SDK for web.
* In this method, you can add the hasTranscodedAudio property to a video media asset to indicate whether it contains transcoded audio.
*/
function transMediaList(data) {
if (!data) return [];
if (Array.isArray(data)) {
return data.map((item) => {
const basicInfo = item.MediaBasicInfo;
const fileBasicInfo = item.FileInfoList[0].FileBasicInfo;
const mediaId = basicInfo.MediaId;
const result = {
mediaId
};
const mediaType = basicInfo.MediaType
result.mediaType = mediaType;
if (mediaType === 'video') {
result.video = {
title: fileBasicInfo.FileName,
...,
// A property that indicates whether transcoded audio is available.
hasTranscodedAudio: !!getTranscodedAudioFileFromFileInfoList(item?.FileInfoList || []),
// If useDynamicSrc is set to false, you must provide the proxy audio URL. Otherwise, this property can be omitted.
agentAudioSrc: '*'
};
...
} else if (mediaType === 'audio') {
...
} else if (mediaType === 'image') {
...
}
return result;
});
} else {
return [data];
}
}
/**
* Retrieves the FileInfo of the transcoded audio from the FileInfoList of a video media asset.
* @param {list<FileInfo>} fileInfoList
* @returns FileInfo | undefined
* The MediaInfo.FileInfoList returned by ListMediaBasicInfos and SearchMedia contains only the source file. In contrast, the list returned by GetMediaInfo and BatchGetMediaInfos contains all streams.
*/
export const getTranscodedAudioFileFromFileInfoList = (fileInfoList = []) => {
if (!fileInfoList.length) return;
// Use FileType === 'transcode_file' to filter for transcoded audio.
const transcodedAudioFiles = fileInfoList.filter((item = {}) => {
return (
item?.FileBasicInfo?.FileType === 'transcode_file' &&
getFileType(item?.FileBasicInfo?.FileName) === MEDIA_TYPE.AUDIO
);
});
if (transcodedAudioFiles.length) {
const mp3FileInfo = fileInfoList.find(
(item = {}) => getFileExtension(item?.FileBasicInfo?.FileName).toUpperCase() === 'MP3'
);
// Prioritize returning the MP3 file.
return mp3FileInfo || transcodedAudioFiles[0];
}
};Loading proxy audio
The Web SDK separates audio tracks from video assets with proxy audio based on whether URLs are loaded dynamically:
For dynamic resources, the video editing SDK for web retrieves the audio URL returned by the getDynamicSrc method after separating the audio track. If a video is marked as having proxy audio or if
config.hasTranscodedAudio=trueis set globally, the SDK unconditionally uses the URL returned by getDynamicSrc. To accelerate loading and audio waveform drawing, ensure your getDynamicSrc method returns the correct audio URL based on themediaTypeparameter.For static resources, you can provide the proxy audio URL directly in the video data by setting
Media.video.agentAudioSrc = {agent-audio-static-src}. If a video is marked as having proxy audio or if you setconfig.hasTranscodedAudio=trueglobally, the SDK unconditionally usesagentAudioSrc || srcfor audio waveform drawing. Therefore, ensuring theagentAudioSrcproperty contains the correct audio URL significantly improves audio loading and waveform drawing speed.
Integrate digital humans
To enable digital humans, update getDynamicSrc and configure avatarConfig.
getDynamicSrc
Adding a digital human generates two video files: the original with a green screen and a black-and-white mask for transparent background compositing. Extract the mask video and pass it to the SDK.
avatarConfig
Parameter
Description
outputConfigs
Sets the output resolution and bitrate for digital human videos.
filterOutputConfig
Filters available output resolutions per digital human. When ListSmartSysAvatarModels returns
OutputMaskasfalse, only 1920×1080 and 1080×1920 are supported.refreshInterval
Task polling interval for digital human synthesis, in milliseconds.
getAvatarList
Calls ListSmartSysAvatarModels to retrieve the list of official digital humans.
submitAvatarVideoJob
Submits a digital human synthesis task. If you use a temporary storage path, configure it in the IMS console first.
getAvatarVideoJob
Gets the digital human synthesis task status. The SDK polls
getAvatarVideoJobat the configured interval. When the task completes, ensure the media library contains both the mask and green screen videos.getAvatar
Gets a digital human by ID.
window.AliyunVideoEditor.init({ // Customize the logic to dynamically get URLs. You must extract the digital human's mask video and provide it to the video editing SDK for web for transparent masking. getDynamicSrc: (mediaId, mediaType) => { return request('GetMediaInfo', { // https://help.aliyun.com/document_detail/197842.html MediaId: mediaId }).then((res) => { // Note: This code is for demonstration purposes only. In production, you should implement robust error handling to manage exceptions, such as when FileInfoList is an empty array. const fileInfoList = get(res, 'data.MediaInfo.FileInfoList', []); let mediaUrl,maskUrl; let sourceFile = fileInfoList.find((item)=>{ return item?.FileBasicInfo?.FileType === 'source_file'; }) if(!sourceFile){ sourceFile = fileInfoList[0] } const maskFile = fileInfoList.find((item)=>{ return ( item.FileBasicInfo && item.FileBasicInfo.FileUrl && item.FileBasicInfo.FileUrl.indexOf('_mask') > 0 ); }); if(maskFile){ maskUrl = get(maskFile,'FileBasicInfo.FileUrl'); } mediaUrl = get(sourceFile,'FileBasicInfo.FileUrl'); if(!maskUrl){ return mediaUrl; } return { url: mediaUrl, maskUrl } }) }, // Digital human configuration avatarConfig: { // Video output resolution and bitrate filterOutputConfig: (item, configs) => { if (item.outputMask === false) { return [ { width: 1920, height: 1080, bitrates: [4000] }, { width: 1080, height: 1920, bitrates: [4000] }, ]; } return configs; }, // Task polling interval in milliseconds refreshInterval: 2000, // Get the list of official digital humans getAvatarList: () => { return [ { id: "default", default: true, name: "Official Digital Human", getItems: async (pageNo, pageSize) => { const res = await requestGet("ListSmartSysAvatarModels", { PageNo: pageNo, PageSize: pageSize, SdkVersion: window.AliyunVideoEditor.version, }); if (res && res.status === 200) { return { total: get(res, "data.TotalCount"), items: get(res, "data.SmartSysAvatarModelList", []).map( (item) => { return { avatarName: item.AvatarName, avatarId: item.AvatarId, coverUrl: item.CoverUrl, videoUrl: item.VideoUrl, outputMask: item.OutputMask, }; } ), }; } return { total: 0, items: [], }; }, }, { id: "custom", default: false, name: "My Digital Human", getItems: async (pageNo, pageSize) => { const res = await requestGet("ListAvatars", { PageNo: pageNo, PageSize: pageSize, SdkVersion: window.AliyunVideoEditor.version, }); if (res && res.status === "200") { const avatarList = get(res, "data.Data.AvatarList", []); const coverMediaIds = avatarList.map((aitem) => { return aitem.Portrait; }); const coverListRes = await requestGet("BatchGetMediaInfos", { MediaIds: coverMediaIds.join(","), AdditionType: "FileInfo", }); const mediaInfos = get(coverListRes, "data.MediaInfos"); const idCoverMapper = mediaInfos.reduce((result, m) => { result[m.MediaId] = get( m, "FileInfoList[0].FileBasicInfo.FileUrl" ); return result; }, {}); return { total: get(res, "data.TotalCount"), items: avatarList.map((item) => { return { avatarName: item.AvatarName || "", avatarId: item.AvatarId, coverUrl: idCoverMapper[item.Portrait], videoUrl: undefined, outputMask: false, transparent: item.Transparent, }; }), }; } return { total: 0, items: [], }; }, }, ]; }, // Submits a digital human synthesis task submitAvatarVideoJob: async (job) => { const storageListReq = await requestGet("GetStorageList"); const tempFileStorageLocation = storageListReq.data.StorageInfoList.find((item) => { return item.EditingTempFileStorage; }); if (tempFileStorageLocation) { const { StorageLocation, Path } = tempFileStorageLocation; /** * Checks if the digital human output format supports a transparent background. * outputMask (boolean): Specifies whether to output a mask video. If true, the output video must be in MP4 format. A mask video and a solid-background MP4 video are generated. * transparent (boolean): Specifies whether the video is transparent. If false, the digital human has a background and a WebM video with a transparent background cannot be generated. * */ const { outputMask, transparent } = job.avatar; const filename = outputMask || transparent === false ? `${encodeURIComponent(job.title)}-${Date.now()}.mp4` : `${encodeURIComponent(job.title)}-${Date.now()}.webm`; const outputUrl = `https://${StorageLocation}/${Path}${filename}`; const params = { UserData: JSON.stringify(job), }; if (job.type === "text") { params.InputConfig = JSON.stringify({ Text: job.data.text, }); params.EditingConfig = JSON.stringify({ AvatarId: job.avatar.avatarId, Voice: job.data.params.voice, // Speaker. Required for text input. SpeechRate: job.data.params.speechRate, // Speech rate. Valid for text input only. Range: -500 to 500. Default: 0. PitchRate: job.data.params.pitchRate, // Pitch rate. Valid for text input only. Range: -500 to 500. Default: 0. Volume: job.data.params.volume, }); params.OutputConfig = JSON.stringify({ MediaURL: outputUrl, Bitrate: job.data.output.bitrate, Width: job.data.output.width, Height: job.data.output.height, }); } else { params.InputConfig = JSON.stringify({ MediaId: job.data.mediaId, }); params.EditingConfig = JSON.stringify({ AvatarId: job.avatar.avatarId, }); params.OutputConfig = JSON.stringify({ MediaURL: outputUrl, Bitrate: job.data.output.bitrate, Width: job.data.output.width, Height: job.data.output.height, }); } const res = await request("SubmitAvatarVideoJob", params); if (res.status === 200) { return { jobId: res.data.JobId, mediaId: res.data.MediaId, }; } else { throw new Error("Failed to submit task."); } } else { throw new Error("Failed to get temporary path."); } }, // Gets the status of the digital human synthesis task. getAvatarVideoJob: async (jobId) => { try { const res = await requestGet("GetSmartHandleJob", { JobId: jobId }); if (res.status !== 200) { throw new Error( `response error:${res.data && res.data.ErrorMsg}` ); } let job; if (res.data.UserData) { job = JSON.parse(res.data.UserData); } let video; let done = false; let subtitleClips; // Parse generated subtitles. if (res.data.JobResult && res.data.JobResult.AiResult) { const apiResult = JSON.parse(res.data.JobResult.AiResult); if ( apiResult && apiResult.subtitleClips && typeof apiResult.subtitleClips === "string" ) { subtitleClips = JSON.parse(apiResult.subtitleClips); } } const mediaId = res.data.JobResult.MediaId; if (res.data.State === "Finished") { // Get the status of the media asset. const res2 = await request("GetMediaInfo", { MediaId: mediaId, }); if (res2.status !== 200) { throw new Error( `response error:${res2.data && res2.data.ErrorMsg}` ); } // Check if the video and mask video were generated successfully. const fileLength = get( res2, "data.MediaInfo.FileInfoList", [] ).length; const { avatar } = job; const statusOk = get(res2, "data.MediaInfo.MediaBasicInfo.Status") === "Normal" && (avatar.outputMask ? fileLength >= 2 : fileLength > 0); const result = statusOk ? transMediaList([get(res2, "data.MediaInfo")]) : []; video = result[0]; done = !!video && statusOk; if (done) { // Associate the new digital human asset with the project. await request("AddEditingProjectMaterials", { ProjectId: projectId, MaterialMaps: JSON.stringify({ video: mediaId, }), }); } } else if (res.data.State === "Failed") { return { done: false, jobId, mediaId, job, errorMessage: `job status fail,status:${res.data.State}`, }; } // Return the task status. Polling stops when 'done' is true. return { done, jobId: res.data.JobId, mediaId, job, video, subtitleClips, }; } catch (ex) { return { done: false, jobId, errorMessage: ex.message, }; } }, getAvatar: async (id) => { const listRes = await requestGet("ListSmartSysAvatarModels", { SdkVersion: window.AliyunVideoEditor.version, PageNo: 1, PageSize: 100, }); const sysAvatar = get( listRes, "data.SmartSysAvatarModelList", [] ).find((item) => { return item.AvatarId === id; }); if (sysAvatar) { return { ...objectKeyPascalCaseToCamelCase(sysAvatar), }; } const res = await requestGet("GetAvatar", { AvatarId: id }); const item = get(res, "data.Data.Avatar"); const coverListRes = await request("BatchGetMediaInfos", { MediaIds: item.Portrait, AdditionType: "FileInfo", }); const mediaInfos = get(coverListRes, "data.MediaInfos"); const idCoverMapper = mediaInfos.reduce((result, m) => { result[m.MediaId] = get(m, "FileInfoList[0].FileBasicInfo.FileUrl"); return result; }, {}); return { avatarName: item.AvatarName || "test", avatarId: item.AvatarId, coverUrl: idCoverMapper[item.Portrait], videoUrl: undefined, outputMask: false, transparent: item.Transparent, }; }, }, })
Custom voice
export const transVoiceGroups = (data = []) => {
return data.map(({ Type: type, VoiceList = [] }) => {
return {
type,
voiceList: VoiceList.map((item) => {
const obj = {};
Object.keys(item).forEach((key) => {
obj[lowerFirst(key)] = item[key];
});
return obj;
}),
};
});
};
const customVoiceGroups= await requestGet('ListSmartVoiceGroups').then((res)=>{
const commonItems = transVoiceGroups(get(res, 'data.VoiceGroups', []));
const customItems = [
{
type: 'Basic',
category: 'Dedicated voice', // The dedicated voice category is supported in v4.12.0 and later.
emptyContent: {
description: 'No voices available.',
link: '',
linkText: 'Create a dedicated voice',
},
getVoiceList: async (page, pageSize) => {
const custRes = await requestGet('ListCustomizedVoices',{ PageNo: page, PageSize: pageSize });
const items = get(custRes, 'data.Data.CustomizedVoiceList');
const total = get(custRes, 'data.Data.Total');
const kv = {
story: 'Story',
interaction: 'Interaction',
navigation: 'Navigation',
};
return {
items: items.map((it) => {
return {
desc: it.VoiceDesc || kv[it.Scenario] || it.Scenario,
voiceType: it.Gender === 'male' ? 'Male' : 'Female',
voiceUrl: it.VoiceUrl || '',
tag: it.VoiceDesc || it.Scenario,
voice: it.VoiceId,
name: it.VoiceName || it.VoiceId,
remark: it.Scenario,
demoMediaId: it.DemoAudioMediaId,
custom: true,
};
}),
total,
};
},
getVoice: async (voiceId) => {
const custRes = await requestGet('GetCustomizedVoice',{ VoiceId: voiceId });
const item = get(custRes, 'data.Data.CustomizedVoice');
const kv = {
story: 'Story',
interaction: 'Interaction',
navigation: 'Navigation',
};
return {
desc: item.VoiceDesc || kv[item.Scenario] || item.Scenario,
voiceType: item.Gender === 'male' ? 'Male' : 'Female',
voiceUrl: item.VoiceUrl || '',
tag: it.VoiceDesc || item.Scenario,
voice: item.VoiceId,
name: item.VoiceName || item.VoiceId,
remark: item.Scenario,
demoMediaId: item.DemoAudioMediaId,
custom: true,
};
},
getDemo: async (mediaId) => {
const mediaInfo = await requestGet('GetMediaInfo',{ MediaId: mediaId });
const src = get(mediaInfo, 'data.MediaInfo.FileInfoList[0].FileBasicInfo.FileUrl');
return {
src: src,
};
},
},
{
type: 'General',
category: 'Dedicated voice',
emptyContent: {
description: 'No voices available.',
link: '',
linkText: 'Create a dedicated voice',
},
getVoiceList: async (page, pageSize) => {
const custRes = await requestGet('ListCustomizedVoices',{ PageNo: page, PageSize: pageSize, Type: 'Standard', });
const items = get(custRes, 'data.Data.CustomizedVoiceList');
const total = get(custRes, 'data.Data.Total');
return {
items: items.map((it) => {
return {
desc: it.VoiceDesc,
voiceType: it.Gender === 'male' ? 'Male' : 'Female',
voiceUrl: it.VoiceUrl || '',
tag: it.VoiceDesc,
voice: it.VoiceId,
name: it.VoiceName || it.VoiceId,
remark: it.Scenario,
demoMediaId: it.DemoAudioMediaId,
custom: true,
};
}),
total,
};
},
getVoice: async (voiceId) => {
const custRes = await requestGet('GetCustomizedVoice',{ VoiceId: voiceId });
const item = get(custRes, 'data.Data.CustomizedVoice');
const kv = {
story: 'Story',
interaction: 'Interaction',
navigation: 'Navigation',
};
return {
desc: item.VoiceDesc || kv[item.Scenario] || item.Scenario,
voiceType: item.Gender === 'male' ? 'Male' : 'Female',
voiceUrl: item.VoiceUrl || '',
tag: item.VoiceDesc || item.Scenario,
voice: item.VoiceId,
name: item.VoiceName || item.VoiceId,
remark: item.Scenario,
demoMediaId: item.DemoAudioMediaId,
custom: true,
};
},
getDemo: async (mediaId) => {
const mediaInfo = await requestGet('GetMediaInfo',{ MediaId: mediaId });
const src = get(mediaInfo, 'data.MediaInfo.FileInfoList[0].FileBasicInfo.FileUrl');
return {
src: src,
};
},
},
].concat(commonItems);
return customItems;
})
// Call the init method only after the customVoiceGroups parameter is set.
window.AliyunVideoEditor.init({
...
customVoiceGroups:customVoiceGroups
...
}) Public media asset library
The media library menu is hidden by default. Pass publicMaterials to enable it. See the demo code for the full implementation.
window.AliyunVideoEditor.init({
// Other parameters are omitted.
publicMaterials: {
getLists: async () => {
const resultPromise = [
{
bType: "bgm",
mediaType: "audio",
name: "music",
},
{
bType: "bgi",
mediaType: "image",
styleType: "background",
name: "background",
},
].map(async (item) => {
const res = await request("ListAllPublicMediaTags", {
BusinessType: item.bType,
});
const tagList = get(res, "data.MediaTagList");
return tagList.map((tag) => {
const tagName =
locale === "zh-CN"
? tag.MediaTagNameChinese
: tag.MediaTagNameEnglish;
return {
name: item.name,
key: item.bType,
mediaType: item.mediaType,
styleType: item.styleType,
tag: tagName,
getItems: async (pageNo, pageSize) => {
const itemRes = await request("ListPublicMediaBasicInfos", {
BusinessType: item.bType,
MediaTagId: tag.MediaTagId,
PageNo: pageNo,
PageSize: pageSize,
IncludeFileBasicInfo: true,
});
const total = get(itemRes, "data.TotalCount");
const items = get(itemRes, "data.MediaInfos", []);
const transItems = transMediaList(items);
return {
items: transItems,
end: pageNo * pageSize >= total,
};
},
};
});
});
const resultList = await Promise.all(resultPromise);
const result = resultList.flat();
return result;
},
},
});Asynchronous media asset import
type InputMedia = (InputVideo | InputAudio | InputImage) ;
interface InputSource {
sourceState?: 'ready' | 'loading' | 'fail'; // The state of the media asset. 'loading': The asset is processing and cannot be added to a track. 'fail': Processing failed, and the asset cannot be added to a track. 'ready': The asset is ready for preview and can be added to a track. Default: 'ready'.
}
// For details on data structures, see the integration documentation.
// When you import a media asset that requires asynchronous processing, such as transcoding or image sprite generation, you can set its initial state to 'loading'.
// This example shows how to use 'searchMedia' to import a third-party media asset from a URL and set its initial state to 'loading'.
searchMedia:async()=>{
// 1. Select a third-party media asset.
// 2. Call RegisterMediaInfo to register the asset with the media asset library and get its media asset ID.
// 3. Call GetMediaInfo to retrieve the registered asset's information; its state will be 'loading'.
//.....
return [
{
mediaId: "https://xxxx.xxxxx.mp4",
mediaType: "video",
mediaIdType: "mediaURL",
sourceState: "loading",
video: {
title: "tettesttsete",
coverUrl:"https://xxxxxx.jpg",
duration: 10,
},
},
]
}
// After the image sprite is generated for the third-party media asset, update the asset's state.
AliyunVideoEditor.updateProjectMaterials((old) => {
return old.map((item) => {
if (item.mediaId === mediaId) {
if ("video" in item) {
item.video.spriteConfig = {
num: "32",
lines: "10",
cols: "10",
};
item.video.sprites = [image]; // 'image' is the URL of the generated image sprite.
item.sourceState = "ready";
}
}
return item;
});
});Video translation
Module | Parameter | Description |
translation | video translation | Integrates with the backend API for video translation. SubmitVideoTranslationJob. |
detext | subtitle erasure | Integrates with the backend API to erase subtitles from a video. SubmitIProductionJob. |
captionExtraction | subtitle extraction | Integrates with the backend API to extract subtitles from a video. SubmitIProductionJob. |
Example:
window.AliyunVideoEditor.init({
// Other parameters are omitted.
videoTranslation: {
translation: {
submitVideoTranslationJob: async (params) => {
// This example shows how to retrieve a temporary storage address. Implement this logic to meet your business needs.
const tempFileStorageLocation = await getTempFileLocation();
if (!tempFileStorageLocation) {
return {
jobDone: false,
jobError: 'Specify a temporary storage address.' ,
};
}
const item = tempFileStorageLocation;
const path = item.Path;
if (params.editingConfig.SourceLanguage !== 'zh') {
return {
jobDone: false,
jobError: 'Only translation from Chinese is supported.' ,
};
}
if (params.type === 'Video') { // Translate a video.
const storageType = item.StorageType;
let outputConfig = {
MediaURL: `https://${item.StorageLocation}/${path}videoTranslation-${params.mediaId}.mp4`,
};
if (storageType === 'vod_oss_bucket') {
outputConfig = {
OutputTarget: 'vod',
StorageLocation: get(item, 'StorageLocation'),
FileName: `videoTranslation-${params.mediaId}.mp4`,
TemplateGroupId: 'VOD_NO_TRANSCODE',
};
}
const res = await request("SubmitVideoTranslationJob",{
InputConfig: JSON.stringify({
Type: params.type,
Media: params.mediaId,
}),
OutputConfig: JSON.stringify(outputConfig),
EditingConfig: JSON.stringify(params.editingConfig),
});
return {
jobDone: false,
jobId: res.data.Data.JobId,
};
}
if (params.type === 'Text') {// Translate a single subtitle.
const res = await request("SubmitVideoTranslationJob",{
InputConfig: JSON.stringify({
Type: params.type,
Text: params.text,
}),
EditingConfig: JSON.stringify(params.editingConfig),
});
return {
jobDone: false,
jobId: res.data.Data.JobId,
};
}
if (params.type === 'TextArray') {// Translate an array of subtitles.
const res = await request("SubmitVideoTranslationJob",{
InputConfig: JSON.stringify({
Type: params.type,
TextArray: JSON.stringify(params.textArray),
}),
EditingConfig: JSON.stringify(params.editingConfig),
});
return {
jobDone: false,
jobId: res.data.Data.JobId,
};
}
return {
jobDone: false,
jobError: 'Unsupported type',
};
},
getVideoTranslationJob: async (jobId) => {
const resp = await request("GetSmartHandleJob",{
JobId: jobId,
});
const res = resp.data;
if (res.State === 'Executing' || res.State === 'Created') {
return {
jobDone: false,
jobId,
};
}
if (res.State === 'Failed') {
return {
jobDone: true,
jobId,
jobError: 'Job execution failed' ,
};
}
let isJobDone = true;
let text;
let textArray;
let timeline;
let jobError;
if (res.JobResult.AiResult) {
const aiResult = JSON.parse(res.JobResult.AiResult);
const projectId1 = aiResult.EditingProjectId;
if (projectId1) {
const projectRes = await request('GetEditingProject',{
ProjectId: projectId1,
RequestSource: 'WebSDK',
});
const timelineConvertStatus = get(projectRes, 'data.Project.TimelineConvertStatus');
if (timelineConvertStatus === 'ConvertFailed') {
jobError = 'Job execution failed';
} else if (timelineConvertStatus === 'Converted') {
isJobDone = true;
} else {
isJobDone = false;
}
timeline = projectRes.data.Project.Timeline;
}
text = JSON.parse(res.JobResult.AiResult).TranslatedText;
textArray = JSON.parse(res.JobResult.AiResult).TranslatedTextArray;
}
return {
jobDone: isJobDone,
jobError,
jobId,
result: {
text,
textArray,
timeline,
},
};
},
},
detext: {
submitDetextJob: async ({ mediaId, mediaIdType, box }) => {
const tempFileStorageLocation = await getTempFileLocation();
if (!tempFileStorageLocation) {
return {
jobDone: false,
jobError: 'Specify a temporary storage address.' ,
};
}
const item = tempFileStorageLocation;
const path = item.Path;
const res = await request("SubmitIProductionJob",{
FunctionName: 'VideoDetext',
Input: JSON.stringify({
Type: mediaIdType === 'mediaURL' ? 'OSS' : 'Media',
Media: mediaId,
}),
Output: JSON.stringify({
Type: 'OSS',
Media: `https://${item.StorageLocation}/${path}VideoDetext-${mediaId}.mp4`,
}),
JobParams:
box && box !== 'auto'
? JSON.stringify({
Boxes: JSON.stringify(box),
})
: undefined,
});
return {
jobDone: false,
jobId: res.data.JobId,
};
},
getDetextJob: async (jobId) => {
const resp = await request("QueryIProductionJob",{ JobId: jobId });
const res = resp.data;
if (res.Status === 'Queuing' || res.Status === 'Analysing') {
return {
jobDone: false,
jobId,
};
}
if (res.Status === 'Fail') {
return {
jobDone: true,
jobId,
jobError: intl.get('job_error').d('Job execution failed'),
};
}
const mediaUrl = resp.data.Output.Media;
const mediaInfoRes = await request("GetMediaInfo",{ InputURL: mediaUrl });
if (mediaInfoRes.code !== '200') {
await request("RegisterMediaInfo",{ InputURL: mediaUrl });
return {
jobDone: false,
jobId,
};
}
const mediaStatus = get(mediaInfoRes, 'data.MediaInfo.MediaBasicInfo.Status');
let isError = false;
let isMediaReady = false;
let inputVideo;
if (mediaStatus === 'Normal') {
const transVideo = transMediaList([get(mediaInfoRes, 'data.MediaInfo')]);
inputVideo = transVideo[0];
isMediaReady = true;
if (!inputVideo) {
isError = true;
}
} else if (mediaStatus && mediaStatus.indexOf('Fail') >= 0) {
isError = true;
}
return {
jobDone: isMediaReady,
jobError: isError ? 'Job execution failed' : undefined,
jobId: res.JobId,
result: {
video: inputVideo,
},
};
},
},
captionExtraction: {
submitCaptionExtractionJob: async ({ mediaId, mediaIdType, box }) => {
const tempFileStorageLocation = await getTempFileLocation();
if (!tempFileStorageLocation) {
return {
jobDone: false,
jobError: 'Specify a temporary storage address.' ,
};
}
const item = tempFileStorageLocation;
const path = item.Path;
let roi;
if (Array.isArray(box) && box.length > 0 && box[0] && box[0].length === 4) {
const [x, y, width, height] = box[0];
roi = [
[y, y + height],
[x, x + width],
];
}
const res = await request('SubmitIProductionJob',{
FunctionName: 'CaptionExtraction',
Input: JSON.stringify({
Type: mediaIdType === 'mediaURL' ? 'OSS' : 'Media',
Media: mediaId,
}),
Output: JSON.stringify({
Type: 'OSS',
Media: `https://${item.StorageLocation}/${path}CaptionExtraction-${mediaId}.srt`,
}),
JobParams:
box && box !== 'auto'
? JSON.stringify({
roi: roi,
})
: undefined,
});
return {
jobDone: false,
jobId: res.data.JobId,
};
},
getCaptionExtractionJob: async (jobId) => {
const resp = await request('QueryIProductionJob',{ JobId: jobId });
const res = resp.data;
if (res.Status === 'Queuing' || res.Status === 'Analysing') {
return {
jobDone: false,
jobId,
};
}
if (res.Status === 'Fail') {
return {
jobDone: true,
jobId,
jobError: 'Job execution failed',
};
}
const mediaUrl = resp.data.OutputUrls[0];
const srtRes = await fetch(mediaUrl.replace('http:', ''));
const srtText = await srtRes.text();
return {
jobDone: true,
jobId: res.JobId,
result: {
srtContent: srtText,
},
};
},
},
}
});