Predidit / canvas_danmaku

简易高性能的flutter弹幕组件
https://pub.dev/packages/canvas_danmaku
MIT License
4 stars 1 forks source link

弹幕控件绘制以后,导致fvp装载的字幕不间断闪烁 #5

Closed MCDFsteve closed 2 weeks ago

MCDFsteve commented 1 month ago

我将DanmakuScreen()放置在buildbody内的任何位置,装载弹幕时字幕都会不间断闪烁(实际上只放置DanmakuScreen()而不调用装载弹幕相关也是如此)。不知道是因为什么原因导致的这个。

https://github.com/user-attachments/assets/98474194-ebd1-4128-88c6-b52a3e592ca6

部分代码:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black, // 设置背景为黑色
      body: Stack(children: [
        Center(
          child: MouseRegion(
            onHover: (_) {
              setState(() {
                _iconOpacity6 = 1.0;
                _handleMouseHover6();
                _isMouseMoving = true;
              }); // 鼠标移动时启动定时器
            }, // 这里使用一个变量来控制图标的透明度
            cursor: _isMouseMoving
                ? SystemMouseCursors.basic
                : SystemMouseCursors.none,
            child: Focus(
              focusNode: _focusNode,
              onKeyEvent: (FocusNode node, KeyEvent event) {
                if (event is KeyDownEvent) {
                  _handleKeyEvent(event.logicalKey);
                  return KeyEventResult.handled; // 确保事件被处理
                }
                return KeyEventResult.ignored;
              },
              child: _controller == null
                  ? const Text('未选择视频', style: TextStyle(color: Colors.white))
                  : FutureBuilder(
                      future: _initializeVideoPlayerFuture,
                      builder: (context, snapshot) {
                        if (snapshot.connectionState == ConnectionState.done) {
                          return GestureDetector(
                            onTap: _togglePlayPause, // 使用鼠标点击事件控制播放/暂停
                            child: Stack(
                              alignment: Alignment.bottomCenter,
                              children: [
                                SizedBox.expand(
                                  child: FittedBox(
                                    fit: BoxFit.contain,
                                    child: SizedBox(
                                      width: _controller!.value.size.width,
                                      height: _controller!.value.size.height,
                                      child: VideoPlayer(_controller!),
                                    ),
                                  ),
                                ),
                                MouseRegion(onEnter: (_) {
                                  setState(() {});
                                }, onExit: (_) {
                                  setState(() {});
                                }),
                                //显示集数名字
                                Stack(
                                  children: [
                                    Container(),
                                    // 弹幕组件
                                    DanmakuScreen(
                                      createdController: (DanmakuController e) {
                                        _controllerdanmaku = e;
                                      },
                                      option: DanmakuOption(
                                        fontSize: 30,
                                      ),
                                    ),

FFprobe扫描的视频信息:

ffprobe version 7.0.1 Copyright (c) 2007-2024 the FFmpeg developers
  built with Apple clang version 15.0.0 (clang-1500.3.9.4)
  configuration: --prefix=/usr/local/Cellar/ffmpeg/7.0.1 --enable-shared --enable-pthreads --enable-version3 --cc=clang --host-cflags= --host-ldflags='-Wl,-ld_classic' --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libaribb24 --enable-libbluray --enable-libdav1d --enable-libharfbuzz --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librist --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libspeex --enable-libsoxr --enable-libzmq --enable-libzimg --disable-libjack --disable-indev=jack --enable-videotoolbox --enable-audiotoolbox
  libavutil      59.  8.100 / 59.  8.100
  libavcodec     61.  3.100 / 61.  3.100
  libavformat    61.  1.100 / 61.  1.100
  libavdevice    61.  1.100 / 61.  1.100
  libavfilter    10.  1.100 / 10.  1.100
  libswscale      8.  1.100 /  8.  1.100
  libswresample   5.  1.100 /  5.  1.100
  libpostproc    58.  1.100 / 58.  1.100
Input #0, matroska,webm, from '/Library/Afolder/番剧/新番/2024-0716/[LoliHouse] Boku no Tsuma wa Kanjou ga Nai - 03 [WebRip 1080p HEVC-10bit AAC SRTx2].mkv/[LoliHouse] Boku no Tsuma wa Kanjou ga Nai - 03 [WebRip 1080p HEVC-10bit AAC SRTx2].mkv':
  Metadata:
    creation_time   : 2024-07-16T08:35:05.000000Z
    ENCODER         : Lavf60.16.100
  Duration: 00:23:41.73, start: 0.000000, bitrate: 2437 kb/s
  Stream #0:0: Video: hevc (Main 10), yuv420p10le(tv, bt709), 1920x1080, SAR 1:1 DAR 16:9, 23.98 fps, 23.98 tbr, 1k tbn (default)
      Metadata:
        BPS             : 2309442
        DURATION        : 00:23:40.086000000
        NUMBER_OF_FRAMES: 34048
        NUMBER_OF_BYTES : 409950899
        _STATISTICS_WRITING_APP: mkvmerge v80.0 ('Roundabout') 64-bit
        _STATISTICS_WRITING_DATE_UTC: 2024-07-16 08:35:05
        _STATISTICS_TAGS: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES
  Stream #0:1(jpn): Audio: aac (LC), 44100 Hz, stereo, fltp (default)
      Metadata:
        BPS             : 128000
        DURATION        : 00:23:40.086000000
        NUMBER_OF_FRAMES: 61158
        NUMBER_OF_BYTES : 22721376
        _STATISTICS_WRITING_APP: mkvmerge v80.0 ('Roundabout') 64-bit
        _STATISTICS_WRITING_DATE_UTC: 2024-07-16 08:35:05
        _STATISTICS_TAGS: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES
  Stream #0:2(chi): Subtitle: subrip (srt) (default)
      Metadata:
        title           : 简体中文
        BPS             : 56
        DURATION        : 00:23:31.931000000
        NUMBER_OF_FRAMES: 358
        NUMBER_OF_BYTES : 9928
        _STATISTICS_WRITING_APP: mkvmerge v80.0 ('Roundabout') 64-bit
        _STATISTICS_WRITING_DATE_UTC: 2024-07-16 08:35:05
        _STATISTICS_TAGS: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES
  Stream #0:3(chi): Subtitle: subrip (srt)
      Metadata:
        title           : 繁體中文
        BPS             : 56
        DURATION        : 00:23:31.931000000
        NUMBER_OF_FRAMES: 358
        NUMBER_OF_BYTES : 9928
        _STATISTICS_WRITING_APP: mkvmerge v80.0 ('Roundabout') 64-bit
        _STATISTICS_WRITING_DATE_UTC: 2024-07-16 08:35:05
        _STATISTICS_TAGS: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES
Predidit commented 1 month ago

我不知道这是什么原因造成的,这部分代码看上去没有什么问题。

这是您的项目fnipaplayer的一部分吗,如果是的话,我明天把那个项目拉到本地看一看,今天有些太晚了。

MCDFsteve commented 1 month ago

我是各种排除之后,发现移除DanmakuScreen()部分以后,字幕就不会继续闪烁了。我的项目是https://www.github.com/MCDFsteve/FnipaPlay

Predidit commented 1 month ago

有测试用的视频和字幕文件吗

MCDFsteve commented 1 month ago

我使用的视频是

magnet:?xt=urn:btih:083e893cc60a5f049ec8210bb2ec2d905809b98b&tr=http://t.nyaatracker.com/announce&tr=http://tracker.kamigami.org:2710/announce&tr=http://share.camoe.cn:8080/announce&tr=http://opentracker.acgnx.se/announce&tr=http://anidex.moe:6969/announce&tr=http://t.acg.rip:6699/announce&tr=https://tr.bangumi.moe:9696/announce&tr=udp://tr.bangumi.moe:6969/announce&tr=http://open.acgtracker.com:1096/announce&tr=udp://tracker.opentrackr.org:1337/announce

这是磁力链接,需要种子下载器下载源文件。其内嵌了srt字幕文件

MCDFsteve commented 1 month ago

同一个视频,我注释掉DanmakuScreen()部分以后,就会恢复正常,不闪烁字幕了。就像这样:

// 弹幕组件
                                    /*DanmakuScreen(
                                      createdController: (DanmakuController e) {
                                        _controllerdanmaku = e;
                                      },
                                      option: DanmakuOption(
                                        fontSize: 30,
                                      ),
                                    ),*/
MCDFsteve commented 1 month ago

然后,我将canvas_danmaku的代码放置到了本地文件夹,不过只是因为我修改了弹幕描边粗细,以及添加了“将黑色弹幕的描边处理为白色”的功能。修改仅限于utils.dart

Predidit commented 1 month ago

我无法使用 fnipaplay 仓库中的代码进行测试,代码中包含错误,danmakuController 没有被初始化。

我尝试使用 fvp 的 example 和 canvas_danmaku 以及您提供的视频文件制作了一个简单的 sample 。没有复现这一问题。

你可以更新一下 fnipaplay 仓库中的代码吗。

此外,fnipaplay 中的代码在我看来似乎有一些结构上的问题。除了抽象和状态管理不是很恰当之外,这似乎是一个 flutter 库的结构,而不是一个 flutter 工程的结构。这对我的分析造成了一定的困难。不过这应该并不影响其正常运行。

MCDFsteve commented 1 month ago

您制作的sample可以让我参考一下吗?

MCDFsteve commented 1 month ago

这个错误我并不清楚是什么原因,本地正常运行的代码我进行了简单的推送(略过了build文件夹)

Predidit commented 1 month ago

sample 如下,视频文件放置在 assets/boku.mkv,点按弹幕测试按钮加载弹幕。

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:canvas_danmaku/canvas_danmaku.dart';
import 'package:fvp/fvp.dart' as fvp;

void main() {
  fvp.registerWith();
  runApp(
    MaterialApp(
      home: _App(),
    ),
  );
}

class _App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        key: const ValueKey<String>('home_page'),
        body: _BumbleBeeRemoteVideo()
      ),
    );
  }
}

class _BumbleBeeRemoteVideo extends StatefulWidget {
  @override
  _BumbleBeeRemoteVideoState createState() => _BumbleBeeRemoteVideoState();
}

class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> {
  late VideoPlayerController _controller;
  late DanmakuController _danmakuController;

  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.asset(
      'assets/boku.mkv',
      videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
    );

    _controller.addListener(() {
      setState(() {});
    });
    _controller.setLooping(true);
    _controller.initialize();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: <Widget>[
          Container(
            padding: const EdgeInsets.all(20),
            child: AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: Stack(
                alignment: Alignment.bottomCenter,
                children: <Widget>[
                  VideoPlayer(_controller),
                  ClosedCaption(text: _controller.value.caption.text),
                  _ControlsOverlay(controller: _controller),
                  VideoProgressIndicator(_controller, allowScrubbing: true),
                  TextButton(onPressed: () {
                    _danmakuController.addDanmaku(DanmakuContentItem('这是一条很长很长很长的弹幕'));
                  }, child: const Text('弹幕测试')),
                  DanmakuScreen(
                    createdController: (e) {
                      _danmakuController = e;
                    },
                    option: DanmakuOption(
                      fontSize: 30,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _ControlsOverlay extends StatelessWidget {
  const _ControlsOverlay({required this.controller});

  static const List<Duration> _exampleCaptionOffsets = <Duration>[
    Duration(seconds: -10),
    Duration(seconds: -3),
    Duration(seconds: -1, milliseconds: -500),
    Duration(milliseconds: -250),
    Duration.zero,
    Duration(milliseconds: 250),
    Duration(seconds: 1, milliseconds: 500),
    Duration(seconds: 3),
    Duration(seconds: 10),
  ];
  static const List<double> _examplePlaybackRates = <double>[
    0.25,
    0.5,
    1.0,
    1.5,
    2.0,
    3.0,
    5.0,
    10.0,
  ];

  final VideoPlayerController controller;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 50),
          reverseDuration: const Duration(milliseconds: 200),
          child: controller.value.isPlaying
              ? const SizedBox.shrink()
              : Container(
                  color: Colors.black26,
                  child: const Center(
                    child: Icon(
                      Icons.play_arrow,
                      color: Colors.white,
                      size: 100.0,
                      semanticLabel: 'Play',
                    ),
                  ),
                ),
        ),
        GestureDetector(
          onTap: () {
            controller.value.isPlaying ? controller.pause() : controller.play();
          },
        ),
        Align(
          alignment: Alignment.topLeft,
          child: PopupMenuButton<Duration>(
            initialValue: controller.value.captionOffset,
            tooltip: 'Caption Offset',
            onSelected: (Duration delay) {
              controller.setCaptionOffset(delay);
            },
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<Duration>>[
                for (final Duration offsetDuration in _exampleCaptionOffsets)
                  PopupMenuItem<Duration>(
                    value: offsetDuration,
                    child: Text('${offsetDuration.inMilliseconds}ms'),
                  )
              ];
            },
            child: Padding(
              padding: const EdgeInsets.symmetric(
                vertical: 12,
                horizontal: 16,
              ),
              child: Text('${controller.value.captionOffset.inMilliseconds}ms'),
            ),
          ),
        ),
        Align(
          alignment: Alignment.topRight,
          child: PopupMenuButton<double>(
            initialValue: controller.value.playbackSpeed,
            tooltip: 'Playback speed',
            onSelected: (double speed) {
              controller.setPlaybackSpeed(speed);
            },
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<double>>[
                for (final double speed in _examplePlaybackRates)
                  PopupMenuItem<double>(
                    value: speed,
                    child: Text('${speed}x'),
                  )
              ];
            },
            child: Padding(
              padding: const EdgeInsets.symmetric(
                vertical: 12,
                horizontal: 16,
              ),
              child: Text('${controller.value.playbackSpeed}x'),
            ),
          ),
        ),
      ],
    );
  }
}
MCDFsteve commented 1 month ago

试过对接dandanplay的弹幕加载吗(在我的代码里是计算hash以后get请求拉取json文件)

Predidit commented 1 month ago

和那个没有什么关系吧,无论如何最后都是调用 addDanmaku 方法。

我的 smaple 在您那边运行正常吗。

如果 fnipaplay 仓库中的代码不是损坏的,我可能可以做一些尝试,但是现在 fnipaplay 仓库中的代码存在错误。

fnipaplay 的页面布局我觉得有些怪怪的,canvas_danmaku 例子中的 Container() 是留给播放器的,而不是现在这样 Stack 里嵌套 Stack。以及闪烁问题会让人首先想到重绘,可能是页面里有什么地方在频繁重绘。

MCDFsteve commented 1 month ago

我将sample代码放进了一个项目的main.dart进行了替换并flutter pub get获取了所有库,那个视频我也改名字为boku.mkv放进了项目根目录的assets文件夹以后。打开的窗口没有出现画面,按 播放 按钮也没啥反应。

image
Predidit commented 1 month ago

pubspec.yaml 没有设置 assets 的访问权限?

MCDFsteve commented 1 month ago
image

我仓库的代码没有问题啊。我从云端拉取到我本地的下载文件夹并在flutter pub get后执行flutter run -d macos,程序正常的工作。

image

另外我给予assets权限以后视频正常播放了,但是字幕一样存在闪烁。

https://github.com/user-attachments/assets/0cc21902-96b0-4658-ab74-63c16350570b

MCDFsteve commented 1 month ago

所以似乎是一个只存在于macOS的问题

Predidit commented 1 month ago

看上去这是一个特定于 MacOS 的问题,因为这个问题无法在 windows 上复现。

canvas_danmaku 是一个纯 dart 库,也就是 canvas_danmaku 中不包含特定于平台的代码。

这可能是一个 video_player 的问题,但我没有办法测试,你可以试试移除 fvp 能否复现,video_player 包有 MacOS 支持,不需要 fvp 的补充。这样可以确认问题来自 video_player 还是 fvp。

如果问题来自 video_player 我的推测是弹幕绘制和字幕绘制都用了相对底层的 CustomPainter ,在一个 Stack 中绘制时触发了 flutter 在 MacOS 上实现的一些奇怪问题。

作为临时的解决方案,可以尝试使用 Overlay 而不是 Stack 。

MCDFsteve commented 1 month ago

但是video_player本身并不支持h265的mkv视频,我也不确定是否支持内嵌srt,移除fvp的话大概率视频本身就无法播放。关于Overlay,我后面尝试一下

Predidit commented 1 month ago

理论上 video_player 在 macOS 的底层是 AVPlayer 是支持 h265 的。

fvp 的字幕功能似乎是自己实现的,而不是依赖于 video_player 。所以我觉得应该确认一下问题来自 fvp 还是 video_player。

MCDFsteve commented 1 month ago

移除fvp以后,播放h265的视频现在会一直转圈圈加载

Predidit commented 1 month ago

可能的解决方案还有

  1. 使用 media_kit而不是 fvp
  2. 禁用fvp字幕,使用其他字幕库。
MCDFsteve commented 1 month ago

media_kit之前使用的时候发现字幕渲染使用的自己的一套规则从而无视ass字幕内部定义的那些样式和特效

Predidit commented 1 month ago

我没有办法向 fvp 或是 video_player 报告这一问题,因为我并没有 MacOS 设备。使用 CustomPainter 叠加在 Player 上方应该就能复现这一问题,如果你有兴趣的话,可以向他们报告,错误报告中不应包含第三方库(也就是 canvas_danmaku),你可能需要自己加一个简易的 CustomPainer。

看上去应该做出一些取舍,弃用 canvas_danmaku 使用 ns_danmaku。或者弃用 fvp 使用 media_kit 。media_kit 可以自定义字幕渲染样式。

此外,我前面提到的使用 Overlay 取代 Stack 或许能解决问题。

Predidit commented 1 month ago

我注意到了一个有趣的新出现的视频播放器 av_media_player

它有着优秀的性能,其实现方案和 fvp 类似,因此它取得了和 fvp 一样优秀的构建体积。

它完全开源,而不是像 fvp 那样使用了大量恼人的私有代码。

字幕方面,它基于 webATT 实现了字幕支持。

我认为对于你的使用场合,这个播放器库似乎是最优解。

可惜对我来说,它还缺乏一些串流网络媒体所需的必要支持。(例如自定义HTTP标头)

MCDFsteve commented 1 month ago

我注意到了一个有趣的新出现的视频播放器 av_media_player

它有着优秀的性能,其实现方案和 fvp 类似,因此它取得了和 fvp 一样优秀的构建体积。

它完全开源,而不是像 fvp 那样使用了大量恼人的私有代码。

字幕方面,它基于 webATT 实现了字幕支持。

我认为对于你的使用场合,这个播放器库似乎是最优解。

可惜对我来说,它还缺乏一些串流网络媒体所需的必要支持。(例如自定义HTTP标头)

谢谢 我找机会看一下 不过我做的本地看番播放器 很多字幕组会压制ass字幕(包含字体)进mkv文件 所以基本上得需要libass或类似功能的才能正常加载这些字幕 webATT听起来是web技术?但是web到现在不支持ass

Predidit commented 1 month ago

我的表述问题,准确来说,它支持显示 webVTT 格式的字幕。

我不知道其是否支持 ass 。

MCDFsteve commented 1 month ago

我的表述问题,准确来说,它支持显示 webVTT 格式的字幕。

我不知道其是否支持 ass 。

我后面试试看

MCDFsteve commented 3 weeks ago

我尝试media_kit以后发现mkv格式的视频无法播放(h265编码和h264编码都试过,一直加载的圈圈),不知道是不是我配置问题(media_kit收到的报错是flutter: Error: PlatformException(VideoError, Failed to load video: Cannot Open, null, null))。另外我也试过canvas_danmaku的绘制从Stack换成Overlay,依旧闪烁。也试过把canvas_danmaku换成ns_danmaku,问题依旧

Predidit commented 3 weeks ago

media_kit 基于 mpv,理论上可以解码 mkv 格式,我不知道为什么会这样。我找到了打开的 Issue media_kit/media_kit#809 这确实是 media_kit 的问题,或许它可以通过升级 mpv 库解决,但是 media_kit 现在缺乏维护。

如果 ns_danmaku 也有问题,那么闪烁问题应该和 CustomPaint 的使用无关。fvp 的字幕功能是自己实现的,这可能是一个垂直同步问题,你可以向 fvp 报告该问题。

作为一个可能的临时缓解措施,尝试在 macOS 上启用实验性的 Impeller 渲染引擎,这或许有用。

MCDFsteve commented 3 weeks ago

media_kit/media_kit#809

我已经报告给了fvp,我去看看实验性的 Impeller 渲染引擎

MCDFsteve commented 3 weeks ago

这样开启?

<dict>
    <key>FlutterEnableImpeller</key>
    <true/>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

运行没有报错,但是依旧闪烁。之前发生问题的flutter版本是3.22,我更新为3.24以后还是一样

Predidit commented 3 weeks ago

是这样的没错,注意一下 macOS 的调试模式和发布模式的配置文件在不同位置就可以了。

其实可以直接用 flutter run --enable-impeller 来尝试 impeller 而不用修改配置文件。

依旧闪烁的话就没办法了,等 fvp 的作者解决吧。

MCDFsteve commented 3 weeks ago

确实是实验性,因为我使用flutter run -d macos --enable-impeller以后播放视频是黑屏状态。只能等待fvp那边的issue结果了

Predidit commented 2 weeks ago

此问题已由 fvp 版本更新解决。