Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ const pluginManifest = {
- 发布到市场的外部依赖应固定版本,并填写 64 位 SHA-256;不要引用会变化的分支或 `latest` 文件。
- `pluginDanmakuRenderers` 必须同时申请 `danmaku.renderer` 和 `script.external`。
- 渲染器当前只支持 `apiVersion: 1` 和 `platforms: ['android', 'ios']` 的子集;请勿提前声明 Windows。
- 声明 `supportsRealtimeAdd: true` 的渲染器必须处理 `add` 消息,并立即显示 `message.item`,不能通过清空整表来实现。
- 读取 `settings.value.rendererSettings` 时必须容忍对象或字段缺失,并忽略未知字段;渲染器专属设置不得改变其他渲染器的默认设置行为。
- 渲染器引用的每个依赖 ID 必须存在于 `pluginManifest.requires`。
- 外部代码的来源、许可和再分发条件由投稿者负责确认,并应在插件 README 中说明。

Expand Down
10 changes: 9 additions & 1 deletion js-plugin-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ const pluginDanmakuRenderers = [
description: '示例 DOM 弹幕引擎',
apiVersion: 1,
platforms: ['android', 'ios'],
supportsRealtimeAdd: true,
requires: ['engine'],
bootstrap: String.raw`
const root = document.getElementById('nipa-danmaku-root');
Expand All @@ -217,6 +218,9 @@ const pluginDanmakuRenderers = [
case 'load':
engine.load(message.items || []);
break;
case 'add':
engine.addRealtime(message.item);
break;
case 'settings':
engine.setOptions(message.value || {});
break;
Expand All @@ -241,6 +245,7 @@ const pluginDanmakuRenderers = [
- `description`:选填,渲染器说明。
- `apiVersion`:选填,默认 `1`;当前宿主只支持 `1`。
- `platforms`:必填,当前只接受 `android`、`ios`,至少填写一个。Windows 尚无 WebView 渲染宿主,声明 `windows` 也不会生效。
- `supportsRealtimeAdd`:选填,默认 `false`。设为 `true` 后,本地发送成功导致的单条列表更新会改发 `add`,适配层必须立即渲染该条弹幕;未声明时仍通过整表 `load` 保持兼容。
- `requires`:选填,引用 `pluginManifest.requires` 的依赖 ID。省略时加载清单中的全部依赖;填写后仍按清单顺序加载选中的依赖。
- `bootstrap`:必填,外部脚本加载完成后执行的适配代码。必须安装 `window.NipaDanmakuRenderer.handle(message)`。

Expand All @@ -250,6 +255,7 @@ const pluginDanmakuRenderers = [
|---|---|---|
| `initialize` | `apiVersion`、`pluginId`、`rendererId` | 初始化协议与身份信息 |
| `load` | `version`、`items` | 弹幕列表变化时推送;`version` 是宿主列表版本 |
| `add` | `item` | 仅向声明 `supportsRealtimeAdd: true` 的渲染器发送本地新弹幕;不会紧接着发送同版本整表 `load` |
| `settings` | `value` | 弹幕显示设置变化时推送 |
| `clock` | `positionSeconds`、`durationSeconds`、`playing`、`playbackRate`、`seekRevision` | 播放时钟,正常播放时最多每 100 ms 推送一次 |
| `dispose` | 无 | 释放引擎、DOM、Observer 和监听器 |
Expand All @@ -260,7 +266,9 @@ const pluginDanmakuRenderers = [
- `color`(CSS `rgb(...)`)、`isMe`;
- 可选的 `senderId`、`danmakuId`、`timestamp`、`source`、`fontSize`、`pool`、`weight`,以及数据源扩展字段。

`settings.value` 当前包含 `visible`、`opacity`、`fontSize`、`fontFamily`、`displayArea`、`scrollDurationSeconds`、`stacking`、`merge`、`blockTop`、`blockBottom`、`blockScroll`、`blockWords`、`timeOffsetSeconds`。
`settings.value` 当前包含 `visible`、`opacity`、`fontSize`、`fontFamily`、`displayArea`、`scrollDurationSeconds`、`stacking`、`merge`、`blockTop`、`blockBottom`、`blockScroll`、`blockWords`、`timeOffsetSeconds`。可选的 `rendererSettings` 是当前渲染器的专属配置对象;目前宿主仅在选择 Titan 时发送,其中包含 `opacity`、`fontSize`、`bold`、`fontBorder`、`fontFamily`、`speedPlus`、`density`、`duration`、`limit`、`preventShade`、`offsetTop`、`offsetBottom`、`maxLength`、`isRecyclingDom`、`isRecyclingModel`、`forbidShrinkState`。适配器应忽略不认识的字段,并为缺失字段保留自身默认值。Titan 的 `speedSync` 固定为 `true`,不作为可持久化设置发送。

Titan 专属设置在宿主中独立持久化,仅在 Titan 被选中时显示;其他渲染器继续使用原有通用设置入口和值域。

适配层需要自行把这些通用消息映射到第三方引擎。可通过以下通道向宿主写日志或报告运行错误:

Expand Down
2 changes: 1 addition & 1 deletion plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
{
"id": "titan_danmaku_renderer",
"name": "JavaScript弹幕引擎",
"version": "1.0.2",
"version": "1.0.5",
"minHostVersion": "1.11.4",
"description": "注意:仅支持 iOS、Android 端;使用实验性 JavaScript 弹幕引擎渲染 NipaPlay 弹幕",
"author": "Retr0",
Expand Down
Binary file modified plugins/.DS_Store
Binary file not shown.
20 changes: 18 additions & 2 deletions plugins/titan_danmaku_renderer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ requires: ['titan-bundle'],
shasum -a 256 dist/titan-bundle.js
```

## Titan 专属设置

当 NipaPlay 选择 Titan 渲染器时,软件会显示独立的 Titan 设置入口,并通过
`settings.value.rendererSettings` 传入适配层。当前支持:

- 不透明度、字号倍率、加粗、描边类型(重墨 / 描边 / 45° 投影)和字体族;
- 滚动速度、密度、基准时长与同屏上限;
- 防挡字幕、顶部/底部偏移、最大长度;
- DOM 回收、模型回收与禁止缩小。

这些设置单独持久化,只在 Titan 被选中时显示和生效,不会覆盖其他弹幕引擎的字号、字体、描边与速度配置。
播放倍速同步固定开启,不提供关闭入口。

## 与播放器的通信

插件适配层创建 `window.NipaDanmakuRenderer.handle(message)`,接收 NipaPlay 推送的消息:
Expand All @@ -66,11 +79,14 @@ shasum -a 256 dist/titan-bundle.js
| --- | --- | --- |
| `initialize` | API 版本、插件 ID、渲染器 ID | 预留协议消息,当前适配层无需额外处理 |
| `load` | 标准化弹幕列表和列表版本 | 转换字段后执行 `clear/reset/addList/seek` |
| `settings` | 可见性、透明度、字号、显示区域、字体、屏蔽和时间偏移 | 当前映射可见性、透明度、显示区域、字体和时间偏移;屏蔽已在 App 侧完成 |
| `add` | 用户刚发送成功的单条标准化弹幕 | 不传 `stime`,调用 `engine.add()` 立即渲染,不清空现有弹幕 |
| `settings` | 通用设置及 `rendererSettings` | 映射通用可见性、透明度、显示区域、时间偏移和全部 Titan 专属设置;屏蔽已在 App 侧完成 |
| `clock` | 播放位置、总时长、播放态、倍速和 seek 版本 | 调用 `play/pause/seek/setSetting` 对齐播放器 |
| `dispose` | 渲染器退出 | 断开 `ResizeObserver` 并释放 Engine |

播放时钟最多每 100 ms 推送一次。弹幕列表只在 `danmakuListVersion` 改变时重新发送,避免每帧跨 WebView 传输全部数据。
播放时钟最多每 100 ms 推送一次。历史弹幕或轨道发生普通变化时,宿主按 `danmakuListVersion` 重新发送整表;用户发送成功时,因为本渲染器声明了 `supportsRealtimeAdd: true`,宿主改发单条 `add`,避免 `clear/reset` 清空正在显示的弹幕。

实时 `add` 不显式传递 `stime`,由 Titan 读取 `timelineSync()` 的当前秒数并标记为实时弹幕。历史 `load` 仍通过 `addList()` 载入,并把 NipaPlay 的秒转换成 Titan 所需的毫秒 `stime`。

## 弹幕字段转换

Expand Down
49 changes: 35 additions & 14 deletions plugins/titan_danmaku_renderer/titan_danmaku_renderer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const pluginManifest = {
id: 'titan_danmaku_renderer',
name: 'JavaScript弹幕引擎',
version: '1.0.2',
version: '1.0.5',
description: '注意:仅支持 iOS、Android 端;使用实验性 JavaScript 弹幕引擎渲染 NipaPlay 弹幕',
author: 'Retr0',
minHostVersion: '1.11.4',
Expand All @@ -22,10 +22,12 @@ const pluginDanmakuRenderers = [
description: 'Titan 弹幕引擎,提取自某知名弹幕视频网。原汁原味,一行未动',
apiVersion: 1,
platforms: ['android', 'ios'],
supportsRealtimeAdd: true,
requires: ['titan-bundle'],
bootstrap: String.raw`
const root = document.getElementById('nipa-danmaku-root');
const simplifiedChineseFontFamily = "-apple-system, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC', 'Source Han Sans SC', sans-serif";
const defaultTitanFontFamily = "SimHei, 'Microsoft JhengHei', Arial, Helvetica, sans-serif";
document.documentElement.lang = 'zh-CN';
document.documentElement.style.webkitLocale = "'zh-CN'";
root.lang = 'zh-CN';
Expand Down Expand Up @@ -64,7 +66,7 @@ const pluginDanmakuRenderers = [
setting: {
visible: true,
opacity: 0.85,
fontFamily: simplifiedChineseFontFamily,
fontFamily: defaultTitanFontFamily,
bold: true,
preventShade: false,
speedPlus: 1,
Expand Down Expand Up @@ -93,15 +95,19 @@ const pluginDanmakuRenderers = [
const match = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i.exec(value || '');
return match ? ((+match[1] << 16) | (+match[2] << 8) | +match[3]) : 0xffffff;
}
function load(items) {
const titanItems = items.map((item, index) => ({
function toTitanItem(item, index, realtime) {
const titanItem = {
text: item.content || '',
stime: (+item.time || 0) * 1000,
mode: typeToMode[item.type] || +item.originalType || 1,
size: +item.fontSize || 25,
color: colorToInt(item.color),
dmid: item.danmakuId || ('nipaplay-' + index),
}));
dmid: item.danmakuId || (realtime ? 'local-' + Date.now() : 'nipaplay-' + index),
};
if (!realtime) titanItem.stime = (+item.time || 0) * 1000;
return titanItem;
}
function load(items) {
const titanItems = items.map((item, index) => toTitanItem(item, index, false));
rollLayer.textContent = '';
engine.clear();
engine.reset();
Expand All @@ -110,16 +116,30 @@ const pluginDanmakuRenderers = [
}
engine.seek(clock.positionSeconds + clock.offsetSeconds);
}
function addRealtime(item) {
engine.add(toTitanItem(item || {}, 0, true));
}
function applySettings(value) {
const rendererSettings = value.rendererSettings || {};
engine.setSetting('visible', value.visible !== false);
engine.setSetting('opacity', value.opacity == null ? 1 : value.opacity);
engine.setSetting('opacity', rendererSettings.opacity == null ? 0.85 : rendererSettings.opacity);
engine.setSetting('area', Math.round((value.displayArea || 1) * 100));
engine.setSetting(
'fontFamily',
value.fontFamily
? value.fontFamily + ', ' + simplifiedChineseFontFamily
: simplifiedChineseFontFamily,
);
engine.setSetting('fontSize', rendererSettings.fontSize == null ? 1 : rendererSettings.fontSize);
engine.setSetting('bold', rendererSettings.bold !== false);
engine.setSetting('fontBorder', rendererSettings.fontBorder == null ? 0 : rendererSettings.fontBorder);
engine.setSetting('fontFamily', rendererSettings.fontFamily || defaultTitanFontFamily);
engine.setSetting('speedPlus', rendererSettings.speedPlus == null ? 1 : rendererSettings.speedPlus);
engine.setSetting('density', rendererSettings.density == null ? 1 : rendererSettings.density);
engine.setSetting('duration', rendererSettings.duration == null ? 4.5 : rendererSettings.duration);
engine.setSetting('limit', rendererSettings.limit == null ? 300 : rendererSettings.limit);
engine.setSetting('speedSync', true);
engine.setSetting('preventShade', rendererSettings.preventShade === true);
engine.setSetting('offsetTop', rendererSettings.offsetTop || 0);
engine.setSetting('offsetBottom', rendererSettings.offsetBottom || 0);
engine.setSetting('maxLength', rendererSettings.maxLength == null ? 50 : rendererSettings.maxLength);
engine.setSetting('isRecyclingDom', rendererSettings.isRecyclingDom !== false);
engine.setSetting('isRecyclingModel', rendererSettings.isRecyclingModel === true);
engine.setSetting('forbidShrinkState', rendererSettings.forbidShrinkState !== false);
const nextOffset = +value.timeOffsetSeconds || 0;
if (clock.offsetSeconds !== nextOffset) {
clock.offsetSeconds = nextOffset;
Expand All @@ -146,6 +166,7 @@ const pluginDanmakuRenderers = [
handle(message) {
switch (message.type) {
case 'load': load(message.items || []); break;
case 'add': addRealtime(message.item); break;
case 'settings': applySettings(message.value || {}); break;
case 'clock': syncClock(message); break;
case 'dispose':
Expand Down
13 changes: 13 additions & 0 deletions scripts/sync_plugins_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,16 @@ def _read_int_property(object_source, name, default=None):
return int(match.group(0))


def _read_bool_property(object_source, name, default=None):
start = _find_property(object_source, name)
if start is None:
return default
match = re.match(r"(?:true|false)\b", object_source[start:])
if not match:
raise ValueError(f"'{name}' must be a boolean")
return match.group(0) == "true"


def _read_string_array_property(object_source, name, default=None):
start = _find_property(object_source, name)
if start is None:
Expand Down Expand Up @@ -332,6 +342,9 @@ def parse_danmaku_renderers(filepath):
"platforms": _read_string_array_property(
object_source, "platforms", []
),
"supportsRealtimeAdd": _read_bool_property(
object_source, "supportsRealtimeAdd", False
),
"requires": _read_string_array_property(
object_source, "requires", None
),
Expand Down
Loading