@ banou/ 媒体玩家
浏览器无法自行打开文件的回放视频播放器 。 它需要一个 read(offset, size) 财务报告和审定财务报表
a 字节长度, 重置成零碎的 MP4, 因为它通过 利巴夫 - 瓦斯姆语, (中文(简体) ),
并制作ASS/SSA字幕与jasub. 没有下载到前面,所以它播放了4GB MKV
HTTP 范围请求, 出自种子, 出自本地磁盘, 出自其他任何可以回答一个
字节范围 .
回放状态运行于 视频.js v10。其皮肤没有被使用: 这里的铬是它自己的.
使用量
import MediaPlayer from '@banou/media-player'
<MediaPlayer
read={(offset, size) => Promise<ArrayBuffer>}
size={fileByteLength}
publicPath="/"
libavWorkerUrl="/libav-worker.js"
jassubWorkerUrl={jassubWorkerUrl}
jassubWasmUrl={jassubWasmUrl}
jassubLegacyWasmUrl={jassubLegacyWasmUrl}
defaultFontUrl={defaultFontUrl}
cjkFontUrl={cjkFontUrl}
title="episode.mkv"
autoplay
/>
read 财务报告和审定财务报表 size 一起出行:通过两次或两次。 玩家用两者都做成铬
在一个黑色的相框和等待, 这是空的状态.
inputToRemuxerInput 从 a 构建对 Blob编号 :File,一个URL(显示其长度在一定范围内)
或您自己的阅读器 :
import { inputToRemuxerInput } from '@banou/media-player'
const source = await inputToRemuxerInput({ blob: file })
const source = await inputToRemuxerInput({ url: 'https://example.com/episode.mkv' })
const source = await inputToRemuxerInput({ length, read })
URL 的源头会随着 readsDownload: true,玩家将选择旁边 read 财务报告和审定财务报表 size: 每一页
它是一个下载,所以音量常态化者然后分析所获取的音频播放而不是取取文件
再来一次.
usePlayer 读取并驱动播放状态从内侧任意位置 MediaPlayer这是唯一的钩子
染色体使用:内置视频.js状态和这个玩家自己的源状态(音轨,缩略图,
索引,准备) 活在一个商店上,所以没有第二个上下文可以到达.
import { usePlayer } from '@banou/media-player'
const paused = usePlayer((state) => state.paused) // subscribes to that field
const player = usePlayer() // no selector: the store, no subscription
player.play()
useSeekThumbnails 财务报告和审定财务报表 usePictureInPicture 用于在捆绑的铬外再利用.
useSeekThumbnails 返回时 { thumbnails, requestThumbnail }预览从开始生成
,直到有人指向搜索栏为止: requestThumbnail(time)
移动覆盖的预览 time 走到队列的前面,走在后面
无论它得到。 过 undefined 当指针去。 捆绑的搜索栏叫它
所以这只是你画的铬 它不能打断已经在飞行的解码器,
所以等待是那个的尾巴 而不是整个积压.
downloadedRanges 绘画字节横跨您已经持有的搜索栏,通过键框映射
索引而非百分比, 因为文件的下载进度不是它的回放进度 :
集装箱上装有标题、字体和附件,完全没有时间.
你的应用软件必须服务的资产
工人们和园丁都是从你们提供的东西中 在跑道上找到的,从不从这些东西中找到
软件包自行解决。 libav的简单文件要复制出来 node_modules,这就是什么
copy-assets 脚本有 jassub 无法复制, 必须由您的捆绑者构建; 见下文 .
src/asset-urls.ts 二者的榜样.
publicPath 是目录 libav的两个主题文件 两者必须在场:
| 文件 | 从 | 当使用时 |
|---|---|---|
libav.wasm | libav-wasm/build/ | 没有 JSPI 的浏览器: Safari, 以及 iOS 上的每个浏览器 |
libav-jspi.wasm | libav-wasm/build/ | Chrome和边缘137+,火狐153+ |
{\fn黑体\fs22\bord1\shad0\3ahbe\4ah00\fscx67\fscy66\2chffffff\3ch808080}libav -wasm在跑步时在他们中间挑 typeof WebAssembly.Suspending === 'function'因此服务
只有一种不会在任何地方失败: 它在选择缺失文件的浏览器上失败了, 因为它
读取为浏览器错误而不是缺失资产。 既服务.
libavWorkerUrl (单位:千美元)libav-wasm/build/worker.js)被单独命名,并且仍然可以复制.
复制所有三个 从libav -wasm 这个包取决于。 在Libav -wasm 0.9.0之前的工人又写了一篇
时间表和报告 timestampOffset,所以重播失败.
cjkFontUrl 这个软件包是自己的 noto-sans-jp.otf (单位:千美元)import cjkFontUrl from '@banou/media-player/noto-sans-jp.otf?no-inline&url'),野多·桑斯JP切入JIS X 0208干地. Jassub的利布斯没有
系统字体回落,所以没有它,日文的样式,其字体没有日文,这就是如何放出歌曲
,以空框形式绘制。 它只在选中的音轨显示 CJK 文本时获取, 仅次于 defaultFontUrl, (中文(简体) ),
只画面部缺乏的格言.
normalizerWorkletUrl 这个软件包是自己的 dist/volume-normalizer-worklet.js,一个没有导入的普通模块
复制罚款或输入为url;见 音量常态器。 。 。 .
jassub 的资产是导入的, 没有复制
jassub 2的工人是进口的ES模块 abslink 财务报告和审定财务报表 lfa-ponyfill 光标加
5个亲戚的兄弟姐妹, 它产下一个自己的被窝住的工人。 复制一个文件出局
node_modules 生成浏览器无法加载的东西,所以捆绑者必须构建出该图. 与
在您自己的来源中, 输入一个查询 :
import jassubWorkerUrl from 'jassub/dist/worker/worker.js?worker&url'
import jassubWasmUrl from 'jassub/dist/wasm/jassub-worker-modern.wasm?no-inline&url'
import jassubLegacyWasmUrl from 'jassub/dist/wasm/jassub-worker.wasm?no-inline&url'
import defaultFontUrl from 'jassub/dist/default.woff2?no-inline&url'
?no-inline 用于任何构造的负载 build.lib 设置: 将每个资产内置为
base64 库模式下的数据url并忽略 assetsInlineLimit 在做的时候,没有它,那两个
以5.9 MB为基数64. src/asset-urls.ts 内
此 repo 是相同的四行, 意在被复制 .
如果你建造与 build.lib 设定,将这四个建在一张单独的通行证中. ?no-inline
覆盖您命名的资产, 它不能覆盖那些 jassub 名称: 它的标记胶水
以 new URL('./wasm/...', import.meta.url) 在工人体内,和自由模式内线
这些也是。 根据这个版本,一个条目完全输入上述四行:
| 构建 | 工人块 | 基数64为blobs |
|---|---|---|
build.lib 设定 | 2 922 207 字节 | 2个 |
没有 build.lib | 110,525字节 | 0 个 |
assetsInlineLimit: 0 和函数表无关,因为 lib 模式决定
在协商之前先排好队 与JS单项的第二个虚拟配置,没有 lib,同样
outDir 生产110 KB 工人,30 KB 线粒体胶水以及两个相干文件作为真实文件.
?no-inline&url 需要类型声明,因为 vite 本身的环境 *?url 只匹配一个描述符
结束于 ?url。见 src/vite-env.d.ts。 。 。 .
这两座建筑在运行时被选中:
| 选项 | 文件 | 当使用时 |
|---|---|---|
jassubWasmUrl | jassub/dist/wasm/jassub-worker-modern.wasm | 在有RELEXED SIMD的地方 |
jassubLegacyWasmUrl | jassub/dist/wasm/jassub-worker.wasm | 到处都是 |
测试是 RELEXED simd, 不是基线 simd: jassub 验证一个模块 i8x16.relaxed_swizzle。 。 。 .
这比普通的SIMD更晚 所以第二个文件不是古老的博物馆作品
萨法里在Jassub 1下的样子 既服务.
装作是 application/wasm: jassub 将其与 instantiateStreaming,哪个
拒绝其它内容类型.
不需要因地而异的隔离。 Jassub 分配共享 WebAssembly.Memory 无条件,
在两台发动机(Chrome 152和Firefox 154测量)的平地上建造
自有线程计数已锁定 crossOriginIsolated因此,没有COOP和COEP 它只是运行单
线起了.
染色体带来自己的尺度,不需要主机页面的根字型. 所有东西都是大小的
反对 --mp-unit,默认为 10px ; 设置在那里以调整整个大小
铬一次.
它会做什么
播放和暂停, 以预览缩略图和密钥框精确的擦除来寻找, 在搜索栏上的章节, 提议跳过打开或结尾、日志曲线上的音量、哑音、回放速度、音轨 选择、字幕音轨选择、图片、全屏和键盘快捷键。 没什么 持续 : 音量和速度从默认的每个负载开始,音轨从 首选语言 或者文件的默认值.
快捷键( 空格或 k, (中文(简体) ), f, (中文(简体) ), m,箭头键,和 < 财务报告和审定财务报表 > 暂停,然后退后或向前一步)
听整个页面,然后将 Ctrl, Cmd 和 Alt 组合留在浏览器中. 框架步骤需要媒体
与 requestVideoFrameCallback,哪个a media 在另一个文档中播放没有;其主机可以通过
stepFrame 在该文件中迈出一步。 过 shortcuts="player"
对于玩家及其他内容: 键然后只从玩家内部进行( 点击图片即可突出焦点)
或当它是全屏的, 空间和箭头 继续滚动页面到其它地方.
章节
分会来自容器,搜索栏将分会画成分会,每分会断 边界。 在预览片中.
过 chapters 他们赢得了所有文件:
<MediaPlayer {...source} chapters={[{ start: 0, end: 88, title: 'Opening' }]} />
他们是 { start, end, title } 在秒,命令和不重叠, 他们不需要覆盖
整个时间。 本地文件自动从 libav 获得, 所以这是给一个知道
章节,容器不.
跳过打开并跳过结束
当一个章节看起来像一个开口或结尾时,一个按钮提议跳过它,出现一个 在章节之前的第二秒 并停留六秒。 它从不跳过自己, 所以错误的猜测成本 没有人会按键而不是跳出剧集.
该猜测取自章节标题,取自实际使用的名称(OP, (中文(简体) ), Opening, (中文(简体) ),
ED, (中文(简体) ), Ending, (中文(简体) ), Credits,并带有一首歌曲的同名词),并在整个文件中延后:
Intro 是没有其他东西声称的开口,是冷开的开口 OP 跟着它.
其章节中没有任何可用名称的文件会回落到形状,其中一章大约
每半跑出时间90秒是开场和结束. 没有比
15秒是永远提供, 和一张碟片 大多是主题没有带来什么, 因为在那里
主题是人们所看到的.
同样的猜测是单独发布的, 对于一个必须知道章节是什么而不加载玩家的页面:
classifyChapters(chapters) 从 @banou/media-player/chapters,它不导入玩家的其他东西,但给出
'opening', (中文(简体) ), 'ending' 或各章未定义.
图片中保存字幕
字幕由Jassub画在视频上方的画布上 图片里的照片是视频
元素和别的东西,所以浏览器无法将两者组合: 平面
requestPictureInPicture() 将裸露的视频放入窗口并放入页面字幕.
该帆布属于Jassub,从第二版开始: 建筑师把它转移给工人
元素完全接受一次,并且 destroy() 从文档中删除。 所以,玩家使一个
字幕LAYER和引擎在它内部挂上一个帆布 每一个管道建设。 任何能达到的
表面必须逐层查看 而不是按住.
所以玩家会自己合成它们. 每个显示的相框被绘制到屏幕外的画布上
上面的字幕画布,和 captureStream() 把它变成一个MediaStream 支持隐藏的视频
元素,是进入窗口的元素。 原始元素继续播放并保留
因为帆布流没有音轨.
镜像元素永远不会被暂停 。 暂停的视频停止渲染它的"媒体史特兰",所以让它失望 反映真实元素冻结窗口: 在屏幕上暂停时查找旧场景 。 这个 运输状态则由媒体会话载出,而这正是窗口读取的内容 播放/暂停按钮.
窗外活过玩家. 玩家在打开窗口时卸载, 打开一个 第二,下一个被安装在同一文档中的玩家在其中播放,形状为 它自己的视频。 因此,一个为下集挂载新玩家的应用程序保持窗口没有 单击,需要新建窗口。 如果没有玩家在那一秒内取出窗口,则会关闭.
如果路径不可用, 玩家将不提供图片控制 .
图片为您不拥有的媒体
浏览器在图片中打开图片只用于单击拥有该图片的文档 <video>,所以一个
远程媒体只有在主机选择接受该点击时才能获得控制 :
// `.frame` sets `pointer-events: none`, so clicks reach the chrome everywhere else
<MediaPlayer
media={media}
pictureInPicture={{ onArmedChange: (armed) => { frame.current!.style.pointerEvents = armed ? 'auto' : '' } }}
>
<iframe ref={frame} className='frame' />
</MediaPlayer>
当指针在控件上方时,铬会让控制器的盒子和
主机被告知它携带了武器,所以它的帧会用这个点击,从里面输入图片.
控制跟随媒体 enterpictureinpicture 财务报告和审定财务报表 leavepictureinpicture 活动,以及
图片中的图片 点击它叫媒体 exitPictureInPicture。 。 。 .
一根手指从不靠在控制器上,然后按住,所以水龙头不能武装它。 提供控制权
只在主指针徘徊时((hover: hover) and (pointer: fine)),一触就隐藏了它
直到一个再次徘徊的指针.
主机页面保存的全屏
在玩家周围全屏显示自己元素的一页, 这样全屏可以超过其中的玩家, 通过
fullscreen数字 :
<div ref={stage}>
<MediaPlayer
{...source}
fullscreen={{
active: stageIsFullscreen,
request: () => document.fullscreenElement ? document.exitFullscreen() : stage.current!.requestFullscreen(),
}}
/>
</div>
玩家然后叫不 requestFullscreen按钮, f 并双击每个呼叫 request 一次,期间
媒体自己的活动,所以页面 requestFullscreen 有媒体的激活, 和按钮
shortcuts="player" 跟着 active,该页面自其 fullscreenchange没有道具
玩家自行全屏显示。 另一文档中的玩家将其全屏通过
<MediaPlayer expose>照镜子来要求的.
音量正常化器
设置菜单中的“ 正常音量” 切换会保持文件本身的正常级别, 并关闭运行良好的内容
因此,突然在对话附近爆炸,对话如出一辙。 你一通过就提供
工作室的url,它打开,除非 观众关闭它或你通过 normalizeVolume={false}数字 :
import normalizerWorkletUrl from '@banou/media-player/volume-normalizer-worklet.js?no-inline&url'
<MediaPlayer
{...source}
normalizerWorkletUrl={normalizerWorkletUrl}
normalizeVolume={saved ?? true}
onNormalizeVolumeChange={save}
/>
玩家一无所存,所以一个想要选择坚持的应用程序可以保存什么 onNormalizeVolumeChange 财务报告和审定财务报表
传回去.
它分两个阶段运作. 一个工人第二次读文件,通过 thumbnailRead 当你通过一个,而且唯一的
在内部 downloadedRanges (对于一个来源 readsDownload 它没有读到任何内容, 并取回音频回放,
在播放头前面约30 s),用WebCodecs解码其音频,学习文件的正常关卡(中位400 ms)
并用无限的外观来计算切口,所以切口开始于突起前:输出的每10 ms是
在正常水平上保持为10个LU,每50至8个LU。 它首先分析玩家附近的东西 然后分析
其余的文件,只保留 1 ms 高音读取,从不录音。 如果分析没有达到
playhead, 对于一个音轨来说, 这个浏览器的 WebCodecs 无法解码, 一个 AudioWorklet 播放一个现场舞台: 一个100ms的探测器
将现在所听到的 5LU的音量 削减到10LU的音量.
两个舞台都无法提升 声音在画面后方播放出10ms,上或下,一旦路由.
Url 命名一个文件,它既是工作人,又是工人的脚本,所以它必须是你的出处服务的一个脚本:
一个CSP说 script-src 'self' 拒绝工人或工人 blob: 或者说 data: url,还有
build.lib 内入资产为 data: 无 ?no-inline工人也必须是同源的,因此从
页面的自有来源 .
第一次开机时 <video> 被通过Web Audio发送到它的后半生,因为元素不能
; 关闭是绕行。 运行等待音频 Context 运行: 一个自动播放规则会阻碍恢复
在页面的下一次点击, 点击或按键, 和视频播放 一如既往,直到那时。 WebKit 被拒绝
(单位:千美元)createMediaElementSource 内 @banou/ponyfill/web-audio),因为路由元素会失去体积并在那里变哑.
一旦开通了开关,音频Context就会拒绝:在那之前,开关读作
从那时起,它读取“无法获取”,元素从未像往常一样经过路由和播放.
首选语言
传递语言以启动文件,并保存查看者选择的内容:
<MediaPlayer
{...source}
preferredAudioLanguage={saved.audio}
preferredSubtitleLanguage={saved.subtitles}
onPreferredAudioLanguageChange={(audio) => save({ ...saved, audio })}
onPreferredSubtitleLanguageChange={(subtitles) => save({ ...saved, subtitles })}
/>
首选是标签或名称(ja, (中文(简体) ), ja-JP, (中文(简体) ), jpn, (中文(简体) ), Japanese, (中文(简体) ), English (US)并报告一个选择的方式
音轨名称,其语言、标记或其他标题,或字幕禁用。 文件开始于全音轨
优先使用的语言,否则默认使用:其中的评论或音频描述不计数。 开始字幕
在全音轨上,在语言, 否则在标语和歌曲 其中之一, 否则默认。 观众选择的胜出
为那个文件。 一个不报告语言的字幕音轨
在标题下,并且只有在字幕打开时,所以下一个文件开始于标题,如果
它有一个.
以观众自己的语言开始,通过它作为两个偏好 alwaysShowSubtitles={false}: 有音频的文件
里面播放有字幕的音频,任何其他人播放里面有字幕的原声. 字幕为
只有在音频用他们的语言播放时才使用,所以除了他们的语言之外,还有一种精选的音频语言保留着这些语言。 这个
原始文件的音轨在 originalLanguage 当源头知道的时候ja 对于动漫),否则文件的默认值,
这是它的第一个音轨: 在一个双音轨文件中,它的第一个音轨是哑弹,只有 originalLanguage 寻出正本.
onAlwaysShowSubtitlesChange 当收视者打开音频所播放语言的字幕时,报告真实性
返回为 alwaysShowSubtitles规则是 startingTracks(audio, subtitles, preferences)从
@banou/media-player/language。 。 。 .
玩家自有的媒体选择不同方式 select
解析,其主机将首选项应用到自己的音轨: preferredSelection(kind, selections, preferences), (中文(简体) ),
从 @banou/media-player/language,命名要按照同一规则选择的选项,选择的音频为
源代码默认, 并且不导入玩家的其他任何信息 . languageTag(pick) 从同一个模块中得出一个选取的 BCP 47
标记 (jpn 给 ja, (中文(简体) ), Português (Brasil) 给 pt-BR),或未定义用于命名任何语言,用于读取的代码
标签.
观众语言中的标签
控件,工具提示,菜单,跳出要约和失败都使用英语. 过 language,一个BCP 47标签,例如:
又一个主机页面显示 :
<MediaPlayer {...source} language="fr" />
它们以玩家最接近的语言显示: 拉丁美洲西班牙语(es-419每张 es-*),巴西语
葡萄牙语pt-BR每张 pt-*),法语,德语,意大利语,俄语,日语,朝鲜语,简化语和传统语
中文( 英文)zh-Hans, (中文(简体) ), zh-Hant土耳其语、波兰语、阿拉伯语、希伯来语、印度尼西亚语、越南语、泰语、印地语、乌克兰语、荷兰语、,
瑞典语,捷克语,希腊语,罗马尼亚语,匈牙利语和马来语,其他为英语. 里面的号码是用标记写的
(单位:千美元)es-AR 给 0,5x, (中文(简体) ), es-419 本身 0.5x),并且跟踪语言在其中被命名,即使标签是英语
(单位:千美元)en-DE 给 0,5x) (中文(简体) ). 一个语言的文本是玩家第一次显示时获取的,并带有FormatJS(@formatjs/intl, (中文(简体) ),
标签是英文,直到到达为止。 玩家给予否 language或者它没有的 也拿不到.
每种语言的栏和搜索栏都左到右:右到左写主机页 dir="ltr" 编辑
玩家的家长.
每种语言都是 src/lib/locales/<tag>.json,按英语键 src/lib/labels.ts; 单位测试每个
到格式JS的文件 verify 对抗那个英国人.
版式
一个包裹. src/ 是演示应用程序, src/lib 是它出版的图书馆, 和应用程序导入它
它的出版名称 所以它是一个诚实的消费者.
-
src/应用程序 :main.tsx, (中文(简体) ),routes/home.tsx。 。 。 . 由vite.config.ts输入build/。 。 。 . -
src/lib/engine/管道,其中没有反应: Media Source feeding, remux, jassub, 缩略图. 发表于@banou/media-player/engine。 。 。 . -
src/lib/remote是从 ANOR 文档驱动的玩家, 结束 奥斯拉。 。 。 . 发表于@banou/media-player/remote。 。 。 .在使玩家成为玩家的文档中,一个道具为谁提供文件的帧, 只有他们,或一个有名的出身:
<MediaPlayer expose read={read} size={size} publicPath="/" {...workers} /> <MediaPlayer expose={{ origin: 'https://anime.fkn.app' }} media={media} />一个服务于一个以上玩家的文档会给每个玩家一个ID,一个嵌入者会要求它的一个 想要吗 :
<MediaPlayer expose={{ id: 'left' }} {...left} /> <MediaPlayer expose={{ id: 'right' }} {...right} />const left = mediaPlayer(iframe, { id: 'left', origin }) const right = mediaPlayer(iframe, { id: 'right', origin })双方都默认为一个未命名的玩家,所以一个有一个从未说过的文档. 不管伯爵是谁, 每个文档每个方向都有一个连接: 上面的 IDs multilex , 服务两次 ID 替换该玩家( 源开关是如何被跟踪的), 并询问无人服务的 id 而只是等待 直到有人这样做.
在嵌入器中,
mediaPlayer手拿回它自己的媒介,一样PlayerMedia形状 播放器驱动,所以它读取,移动,听取,甚至可以交给秒<MediaPlayer>数字 :import { mediaPlayer } from '@banou/media-player/remote' const player = mediaPlayer(iframe, { origin: 'https://torrent.fkn.app' }) await player.ready await player.play() player.currentTime = 30 player.addEventListener('seeked', () => console.log(player.currentTime)) player.destroy()play()以远方元素自己的答案和解,如果镜像在被破坏时被拒绝 在飞行中,一个元素的拒绝 在一个中断的游戏, 所以等待它或抓住它;pause()财务报告和审定财务报表load()吞下他们.读取是同步的,因为最远的一面是镜像: 那边的每一个事件都有一个 快照,一写一动一动一动镜像,然后才能被应用出去.
play()结算 远方元素自己的答案, 所以自动播放拒绝在这里.ready待定 没有人回答和拒绝destroy()或中止signal页:1MessagePort谁的同伴 没有发生任何事件, 所以没有什么能分辨死亡的运输 从慢的,等待必须 被束缚而不是等待出来。 使用signal玩家的一生,或Promise.race([player.ready, timeout])独自等待. 切换来源的玩家或重新装入的玩家文档被跟踪:镜像报告emptied然后是新状态 一个元素的方式 将远程玩家递至一秒钟<MediaPlayer media={player}>之后ready一份没有框架的文件 没有人;exposePlayer(media)做同样的不反应.A 类
<MediaPlayer expose>还载有其各章,因为player.chapters带有chapterschange每个事件之后 更改(一个赤色的元素没有),并将其全屏放入用fullscreen: true: 远处 玩家的按钮,f双击然后发射一发fullscreenrequest每个镜子,在媒体内部 激活并显示player.fullscreen如本页面所设置,直到destroy()给全屏回放.const player = mediaPlayer(iframe, { origin, fullscreen: true }) player.addEventListener('fullscreenrequest', () => document.fullscreenElement ? document.exitFullscreen() : stage.requestFullscreen()) document.addEventListener('fullscreenchange', () => { player.fullscreen = !!document.fullscreenElement }) player.addEventListener('chapterschange', () => console.log(player.chapters))在玩家跳过要约集的一角有自己东西的一页
player.endingOffered = false改为 接受最后的结局 读取是远方玩家自己的单词, 从它的渲染后,有一个chapterschange每次更改后的事件 : 等待错误,然后绘制出角, 和一个玩家 无法接受它(一个赤裸的元素,一个更古老的版本) . -
src/lib/react/玩家组件、其铬和钩子. -
src/lib/labels.ts铬的英文文本,和src/lib/locales/其他语言.
由 vite.lib.config.ts 输入 dist/,这是 npm 出版.
发展
npm install
npm run dev # the demo app on port 4560
npm run build # the app, into build/
npm run build-lib # the library, into dist/
应用打开空玩家:黑色,有铬等. 随处投放文件,单击 选择一个,或粘贴一个 URL.