关于 audio-video
audio-video 是面向 Decentraland 开发的技能,涵盖本地音效、音频流和视频表面,帮助场景创作者选择媒体组件并实现播放、空间音频及状态监测。
主要功能
- 01选择音频与视频组件
- 02播放空间或全局音频文件
- 03从 URL 播放音频流
- 04在网格表面显示视频
- 05监测媒体播放状态
适用场景
- 01为场景交互添加音效
- 02播放背景音乐或电台流
- 03在场景屏幕上显示视频
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ decentraland/ sdk-skills --skill audio-video安装后在 Claude Code 中输入/audio-video确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/audio-catalog.mdmedia-patterns.mdmedia-reference.md详细介绍
Decentraland 中的音频和视频
何时使用哪种媒体组件
| 需求 | 组件 | 主要区别 |
|---|---|---|
| 播放文件中的音效(点击、爆炸、脚步声) | AudioSource | 本地文件、空间音频、单次播放或循环播放 |
| 背景音乐或电台流 | AudioStream | 外部 URL、非空间音频、持续播放 |
| 在表面上播放视频(屏幕、广告牌) | VideoPlayer + Material.Texture.Video | 需要网格来显示视频 |
决策流程:
- 是本地音频文件吗?→
AudioSource - 是流媒体 URL(电台、实时音频)吗?→
AudioStream - 是视频内容吗?→ 在平面/网格上使用
VideoPlayer
音频来源
在代码中引用任何音频文件路径之前,请检查 {baseDir}/references/audio-catalog.md。其中列出了 50 个可免费下载的 Decentraland 音频片段,并提供可直接下载的 URL,涵盖了大多数需求(UI 点击声、环境音、音乐、游戏机制音效、声音效果)。
用户提出音效需求时,预期的工作流程如下:
- 阅读本技能说明和
references/audio-catalog.md。 - 如果目录中有合适的片段,将其作为建议提供给用户——说明片段名称及其用途。
- 询问用户想如何继续。有些创作者希望下载目录中的片段;另一些则更喜欢使用占位路径,以便之后自行添加文件。不要擅自假设。
- 如果用户选择目录中的片段:使用
curl -o assets/Audio/<name>.mp3 "<URL>"下载——这些 URL 可直接通过Bash使用,无需单独的工具。 - 如果用户想使用占位文件:使用清晰的占位路径(例如
assets/Audio/<name>.mp3),并告知用户应将哪些文件放到哪里。 - 在
AudioSource.audioClipUrl中引用生成的本地路径。
需要避免的做法:
- 告诉用户“我无法下载音频文件”。目录中的 URL 可通过
Bash+curl正常使用——如果用户需要,这项能力可用。 - 未先检查目录中是否已有合适片段,就推荐外部来源(freesound / mixkit / pixabay)。
- 未经询问就下载片段——即使目录中有完全匹配的片段,也要先确认,再将文件下载到项目中。
AudioSource(音效和音乐)
将其附加到任何实体上,以播放定位音频。字段:audioClipUrl: string(本地文件路径,必需)、playing?: boolean、loop?: boolean、volume?: number(默认值为 1.0)、pitch?: number(播放速度,默认值为 1.0)、currentTime?: number(以秒为单位的播放位置,默认值为 0)、global?: boolean。音频文件放在 assets/Audio/ 中。支持的格式:.mp3(推荐用于音乐)、.ogg(推荐用于音效,文件更小)、.wav。请保持音频文件较小——大文件会增加场景加载时间。16 位 .wav 文件的大小是同一片段 MP3/OGG 格式的数倍——添加前请先转换(ffmpeg -i in.wav -b:a 128k out.mp3)。
音频默认具有空间感——音量会随与实体的距离增加而降低。设置 global: true 可使用非空间音频(各处音量相同)。
每个 AudioSource 实体都需要 Transform——包括设置了 global: true 的实体。 Unity Explorer 只会为具有 TransformComponent 的实体创建 Unity 音频源(UpdateAudioSourceSystem.CreateAudioSource 查询,unity-explorer main 2026-10)。如果没有 TransformComponent,SDK、构建流程和无头引擎都会接受该组件,但音频始终不会播放,也不会记录错误。global 会改变空间化方式,但不会取消这一前提。请在 AudioSource.create 之前调用 Transform.create(entity)(位置可任意)——同一项目中,后来发现静音的风声底噪和静音的语音提示都属于这种情况(2026-10)。该规则已列入 create-scene/references/entity-validation-rules.md 的验证规则中。
空间衰减范围非常大——global: false 并不表示“只在这个房间内”。 PBAudioSource 完全没有距离字段,而 Explorer 只在池化的 Unity AudioSource 上设置了 spatialize/spatialBlend/dopplerLevel,距离曲线则保留 Unity 的默认值(对数曲线,minDistance 为 1 m,maxDistance 为 500 m)——已在 unity-explorer 的 AudioSourceExtensions.FromPBAudioSourceWithClip + AudioSourcesPlugin 中,以及 audio_source.gen.d.ts(7.30.1)中核实。因此,几个音量较低的房间环境音都会泄漏到每个房间,让人感觉“声音无处不在”(所有者报告的症状,2026-10-10)。
如果环境音必须限制在一个房间内,请在系统中自行根据与玩家的距离进行控制:超出半径时设为 playing: false 和 volume: 0,进入半径后逐渐提高音量。合适的半径取决于房间大小、片段和它的 volume——请在场景中调整。限制系统运行频率,并且只在值发生变化时写入。仅在代码中验证了修复——未在场景中重新试听。
重新触发(每次点击都再次播放声音): 使用辅助函数 AudioSource.playSound(entity, clipUrl, resetCursor?)——不要手动修改 getMutable().playing。playSound 会写入完整组件(通过 createOrReplace/getMutableOrNull),因此即使参数完全相同,也能可靠地重新发出事件。手动设置 getMutable(entity).playing = true(或旧的“先设为 playing=false,再设为 playing=true”的技巧)可能会被 LWW-CRDT 去重机制静默吞掉,因为值没有变化——第二次及之后的触发可能不起作用。stopSound(entity, resetCursor?) 会停止播放。两者的 resetCursor 默认值都是 true(开始/停止时将位置设为 0);传入 false 可从当前 currentTime 继续播放/暂停。
AudioSource.playSound(entity, 'assets/Audio/click.mp3') // retriggers from 0 every call
AudioSource.stopSound(entity) // stops, resets cursor to 0
如果实体没有 AudioSource,这两个辅助函数都会返回 false,因此请先创建组件(例如,在初始化时调用 AudioSource.create(entity, { audioClipUrl, playing: false }))。
一个 AudioSource 一次只能播放一个片段。 对正在播放的实体调用 playSound 会替换当前片段(重新触发测试场景中的 URL 切换情况),因此可能重叠的提示音——点击声、判定音、死亡音——各自都需要独立实体;可能叠加的位置重复音(例如连续点击多个方块)则需要一小组实体轮流使用。此外,还要检查每个片段的时长是否与播放时机相符(ffprobe -v quiet -show_entries format=duration -of csv=p=0 file.mp3):在 2 s 的游戏节拍上播放 10 s 的提示音,要么会被下一个提示音截断,要么会在节拍结束后继续播放(所有者报告的问题,2026-10)。
没有淡入淡出或交叉淡化 API。 PBAudioSource 只有 volume 可用于设置音量,因此,压低音量需要通过系统逐帧将 AudioSource.getMutable(entity).volume 平滑调整到目标值(仅在值发生变化时写入)。若要压低音乐后在曲目中途恢复,请保持 playing: true 并将音量设为 0——或者调用 stopSound(entity, false) 暂停,再调用 playSound(entity, url, false) 恢复,因为 resetCursor 的默认设置会让曲目从头开始。
检测声音何时播放完毕: 非循环片段自然结束时,引擎会将 AudioSource.playing 设回 false。使用只读 getter 轮询,并检测 true → false 的状态转换——切勿使用 getMutable 轮询(这会导致组件每帧都被标记为已修改):
let wasPlaying = false
engine.addSystem(() => {
const isPlaying = AudioSource.get(entity).playing ?? false
if (wasPlaying && !isPlaying) console.log('sound finished') // chain next sound/action here
wasPlaying = isPlaying
})
引擎只会在自然播放完成时翻转此标志——由场景发起的 stopSound() 是你自己的写入操作,而循环播放的片段永远不会翻转此标志。或者,使用 audioEventsSystem 在每次 MediaState 变化时接收回调(MS_PLAYING → MS_READY = 已停止,MS_ERROR = 文件加载失败)——请参阅下方的 Audio Events System 一节。请将这两者都视为通知,而非保证——不要在没有超时回退机制的情况下,构建会因此彻底阻塞的逻辑。在移动端,AudioEvent(PBAudioEvent)随 **v1.14.0(2026 年 9 月)**一同推出,同时发布的还有使音量曲线和空间衰减与 Unity 保持一致的音频对齐改进(文档提交 b4d4c88)——在此之前,移动端的混音效果与桌面端有明显差异,也不会发出音频事件;这与超时回退机制已涵盖的“无播放结束信号”情况相同。
玩家必须先与场景交互(点击),音频才能播放(浏览器的自动播放策略)。如果音频文件需要在玩家交互时立即就绪以便播放,请使用 AssetLoad 组件预加载资源。
在场景启动时播放的声音会在 Explorer 的加载画面后方播放,玩家会错过它。对于欢迎旁白、开场提示音,或需要在玩家首次看到场景时响起的音乐,请等待 EngineInfo.sceneHidden 变为 false(加载画面淡出),再设置 playing: true——请参阅 scene-runtime 技能。
**场景隐藏时暂停音频:**同一个 EngineInfo.getOrNull(engine.RootEntity)?.sceneHidden 标志,在任何全屏 Explorer UI(地图、背包、设置)覆盖场景时也会变为 true。在此状态下暂停或静音音频源,并在 sceneHidden 恢复为 false 时继续播放。
添加音频前:从外部来源获取音频前,请先征求用户确认。
AudioStream(流式播放)
从 URL 流式播放音频(广播、实时流)。主要字段:url(流媒体 URL)、playing、volume。默认不具空间性——在任何位置播放的音量都相同。设置 spatial: true,并使用 spatialMinDistance/spatialMaxDistance,即可根据距离调整音量。
使用 AudioStream.getAudioState(entity) 查询状态;它返回 PBAudioEvent | undefined——这是一个包含 state 字段(MediaState 枚举:MS_PLAYING、MS_ERROR 等)和 timestamp 字段的对象,而不是一个单独的枚举值。读取状态时使用 AudioStream.getAudioState(entity)?.state。如需通过回调接收状态变化,而不是轮询,audioEventsSystem.registerAudioEventsEntity 也适用于 AudioStream 实体(请参阅上方 AudioSource 播放结束检测说明)。
添加流媒体 URL 前:如果用户未提供,请先确认来源。
音频事件系统(audioEventsSystem)
监控 AudioSource 和 AudioStream 媒体状态变化。从 @dcl/sdk/ecs 导入。系统仅在状态变化时触发回调(不会每帧触发)。
import { engine, audioEventsSystem, AudioSource } from '@dcl/sdk/ecs'
const radioEntity = engine.addEntity()
AudioSource.create(radioEntity, { audioClipUrl: 'assets/Audio/music.mp3', playing: true })
audioEventsSystem.registerAudioEventsEntity(radioEntity, (event) => {
// event is PBAudioEvent: { state: MediaState, timestamp: number }
console.log('Audio state changed:', event.state)
})
API(已根据 @dcl/ecs 的提交 f858f905 验证):
audioEventsSystem.registerAudioEventsEntity(entity, callback)——为音频状态变化注册回调。回调接收包含state(MediaState枚举)和timestamp的PBAudioEvent。仅在状态变化时触发。audioEventsSystem.removeAudioEventsEntity(entity)——取消注册回调。audioEventsSystem.hasAudioEventsEntity(entity)——返回boolean。audioEventsSystem.getAudioState(entity)——返回PBAudioEvent | undefined(最新状态)。
MediaState 值: MS_LOADING、MS_READY、MS_PLAYING、MS_PAUSED、MS_STOPPED、MS_ERROR、MS_SEEKING、MS_BUFFERING、MS_NONE。
如果实体被移除,或不再具有 AudioSource/AudioStream 组件,该实体会自动取消注册。适用于具有 AudioSource 或 AudioStream 的实体(渲染器会为具有这些组件的实体添加底层 AudioEvent 组件)。
与 AudioStream.getAudioState 的关系: AudioStream.getAudioState 是 AudioStream 组件本身提供的便捷封装;audioEventsSystem.getAudioState 读取底层 AudioEvent 组件,适用于 AudioSource 和 AudioStream。需要通过回调监控状态,或使用 AudioSource 时,请使用 audioEventsSystem。
VideoPlayer
在表面上播放视频。关键字段:src(URL 或本地路径)、playing、loop、volume、playbackRate、position(起始时间,单位为秒)。默认采用非空间音频——设置 spatial: true,并配置最小/最大距离,即可启用定位音频。
设置需要 3 个步骤:使用 MeshRenderer.setPlane() 创建实体,添加 VideoPlayer,创建 Material.Texture.Video({ videoPlayerEntity }) 并应用到材质。使用 Material.setBasicMaterial(推荐,性能更好),或使用带有 emissive 的 Material.setPbrMaterial,让屏幕更亮。
使用 videoEventsSystem 监控播放状态:
videoEventsSystem.registerVideoEventsEntity(entity, callback)——每次状态变化时,回调都会接收PBVideoEvent。videoEventsSystem.removeVideoEventsEntity(entity)——取消注册回调。videoEventsSystem.hasVideoEventsEntity(entity): boolean——检查实体是否已注册。videoEventsSystem.getVideoState(entity): PBVideoEvent | undefined——无需回调即可轮询最新状态。
状态:VS_READY、VS_PLAYING、VS_PAUSED、VS_ERROR、VS_BUFFERING。
重新注册会保留上次报告的状态: 对已注册的实体调用 registerVideoEventsEntity 会替换回调,但会保留上次报告的状态,因此新回调不会再次收到已报告的状态变化。assetLoadLoadingStateSystem.registerAssetLoadLoadingStateEntity 也有相同行为——重新注册会保留已报告事件的计数,从而避免重复回调。已根据 js-sdk-toolchain 提交 9055b4b4 验证。
在多个屏幕的 Material.Texture.Video() 调用中引用同一个 videoPlayerEntity,即可让这些屏幕共用一个 VideoPlayer。
要在非基本形状(曲面屏幕)上播放视频,请使用 GltfNodeModifiers 替换 GLTF 模型的材质。
免费音频文件
音频目录是首选查找位置——请参阅此技能顶部的 音频来源 部分。目录列出了 50 段免费的 Decentraland 音频片段,涵盖音乐、环境音、交互音效、声音效果和游戏机制,每段都附有可直接用于 curl 的 URL。
推荐音频前,请先阅读 {baseDir}/references/audio-catalog.md,确保建议具体可行,然后询问用户是想下载这些片段,还是更倾向于使用占位内容。
重要提示:
AudioSource仅适用于本地文件。切勿将外部 URL 用作audioClipUrl。务必先将文件下载到assets/Audio/。
目录中没有的环境录音
目录中没有环境机械声,例如滴水声、电机声、风扇声、冰箱嗡鸣声和扶梯声。不要用 ffmpeg/python 合成替代音频:创作者想要环境音时,一听到音调发生器的声音就会立刻拒绝(观察记录于 2026-10-10)。Wikimedia Commons 是获取真实录音的可行来源:
- 搜索:
https://commons.wikimedia.org/w/api.php?action=query&list=search&srnamespace=6&srsearch=<terms>+filetype:audio&format=json,然后对搜索结果使用action=query&prop=imageinfo&iiprop=url|extmetadata,读取LicenseShortName和Artist。 - 优先选择 CC0 / 公有领域;CC BY 和 CC BY-SA 必须在场景发布的任何位置附上署名行。下载时,将标题、作者、许可证和来源 URL 记录在场景的
design/说明中。 - 发送描述性
User-Agent,并控制请求频率——快速连续请求约 8 次后,API 返回了 HTTP 429。 - 名为
LL-Q1860 …的文件是 Lingua Libre 单词发音,不是声音效果——请跳过。 - 将音频放入
assets/Audio/前,使用ffmpeg(通过afade淡入/淡出和loudnorm)剪辑成循环片段。
资源文件夹约定
- 默认位置:自行下载的音频存放于
assets/Audio/。 - 旧场景可能已将音频存放在
assets/scene/Audio/下——此路径仍然可用;在这些场景中添加新片段时,请沿用该路径,不要另建平行的assets/Audio/文件夹。 - Creator Hub 资源:通过 Creator Hub UI 直接导入的音频会放在
assets/Audio/(与标准路径相同)。免费 DCL 资源包中的项目会放在assets/asset-packs/,自定义项目则放在assets/custom/。旧场景也可能将用户导入的资源直接放在assets/scene/下。请按原路径引用这些资源,切勿移动或重命名。
决定将新文件放在哪里之前,务必检查场景中现有的文件夹。
场景已在 Creator Hub 中打开时的最快方法
将媒体文件从 Local Assets 标签页拖到视口中,会生成与该文件关联的目录 Smart Item(creator-hub 062ed0a2)——.mp3 / .ogg / .wav 会生成 Ambient Sound - Forest Birds,并重新指向该文件的 AudioSource.audioClipUrl;.mp4 会生成 Video Screen,并重新指向 VideoPlayer.src 和 asset-packs::VideoScreen.defaultURL。只需一个操作,无需先创建实体再添加组件,而且生成的实体会带有完整组件集和手动放置项目的基本视图配置。用户正在键盘前操作时,优先使用此方法,而不是手动构建实体;资源 ID 和对应的工具调用请参阅 creator-hub-mcp。
⚠️ 仅设置 VideoPlayer.src 对 Video Screen 项目来说还不够。 管理员消息总线会在运行时通过 asset-packs::VideoScreen.defaultURL 重新设置 src,因此如果只设置 src 而不更新 defaultURL,它会被模板的流 URL 悄然替换。
⚠️ 只有在 Bevy 渲染器下,Creator Hub 视口才有静音切换按钮(工具栏中的扬声器图标:Mute scene audio / Unmute scene audio)。它会将每个 AudioSource / AudioStream 的音量设为 0,且重新加载后仍会保留该状态。使用 Babylon 时,编辑器根本不会播放场景音频。因此,编辑器中没有声音并不能说明问题——修改 volume 或 playing 前,请先在真实预览中确认音频。
音频响应场景(可视化效果、节拍同步)
如需获取任何 AudioSource、AudioStream 或 VideoPlayer 的实时振幅和频段数据,请使用专用的 audio-analysis 技能。该技能介绍了用于音乐可视化效果、均衡器条和响应式灯光/粒子的 AudioAnalysis 组件(仅适用于 Unity-explorer)。
外部媒体权限
[LEGACY] 外部音频/视频 URL 不需要 ALLOW_MEDIA_HOSTNAMES 权限。@dcl/schemas 中仍保留了该权限及其 allowedMediaHostnames 列表,但目前没有客户端会强制执行该权限——unity-explorer 的主机名检查受 CHECK_ALLOWED_MEDIA_HOSTNAMES 编译定义控制(该定义未设置在任何构建配置中,因此 SceneData.TryGetMediaUrl 只会检查 URL 语法),而 bevy-explorer 没有强制执行。只有已退役的 Web 客户端曾强制执行该权限。新场景不要添加此权限;当前客户端无需该权限即可播放外部媒体。
视频限制与提示
- 同时播放的视频数量:避免同时播放多个视频。除非明确要求,否则最多只播放 1 个视频。最大数量取决于各播放器的质量设置(低画质下可能只有 1 个,详见
{baseDir}/references/media-reference.md中的视频限制表),因此请将 1 视为唯一安全下限。 - 必须使用 HTTPS:视频源必须是 HTTPS URL;HTTP 无法使用
- 基于距离的控制:玩家距离较远时暂停视频,以节省带宽
- 支持的格式:
.mp4(H.264)、.webm、用于直播的 HLS(.m3u8) - 直播:使用 HLS(
.m3u8)URL,这是跨客户端最可靠的方式
示例场景
引擎团队针对真实的 explorer 测试的场景:
- audio-source-retrigger-test —
AudioSource.playSound/stopSound、相同 URL 重新触发、在同一实体上切换 URL、resetCursor语义、音量/音高/循环播放的各种设置,以及playSound优于手动修改getMutable的原因(LWW 去重)。 - audio-visualization —
AudioAnalysis音乐可视化效果(参见audio-analysis技能)。 - audio-finish — 通过
playing状态切换和audioEventsSystem回调检测自然播放结束,以及如何区分由场景发起的停止和自然播放结束。 - asset-load — 使用
AssetLoad预加载 mp3,以及纹理、视频和 glb,并通过每个资源的assetLoadLoadingStateSystem状态回调处理状态(包括缺失路径解析为NOT_FOUND)。这是预加载音频的模式,可确保玩家首次点击时音频立即就绪。 - gltfnodemodifier — 在 GLB 而非基础图元上使用
VideoPlayer:将 HLS.m3u8流通过GltfNodeModifiers视频纹理映射到指定 GLTF 节点。这是上述曲面屏幕/非基础图元场景的权威示例。
完整代码示例和实现模式请参阅 {baseDir}/references/media-patterns.md。组件字段详情请参阅 {baseDir}/references/media-reference.md。