Flutter Chromecast控制插件pure_chromecast的使用

Flutter Chromecast 控制插件 pure_chromecast 的使用

简介

本项目是对 pure_chromecast 的克隆版本,并持续维护和进一步开发。该 Dart 包用于将视频播放到 Chromecast 设备。您可以从 pub.dev 上获取该包:https://pub.dev/packages/pure_chromecast

注意:此包目前仍在开发中,API 可能会完全改变。使用时请注意风险。

这是一个简化版的 https://github.com/thibauts/node-castv2-client 的移植版本。

更新 0.2.0:添加了 MDNS 查找功能,现在可以省略 --host 参数,它会询问您要使用的 Chromecast 设备。

在 macOS 上查找 Chromecast 的 IP 地址

这是我在 Mac 上找到 Chromecast IP 地址的方法。虽然不保证适用于所有人,但如果对任何人有帮助,这里是终端命令:

$ dns-sd -B _googlecast local

复制实例名称:

$ dns-sd -L <IntanceName> _googlecast._tcp. local.

复制名称(不包括端口)直接在文本后:

$ dns-sd -Gv4v6 <Paste>

示例实现

您可以查看 https://github.com/terrabythia/flutter_chromecast_example 获取在 Flutter 中使用 flutter_mdns_plugin 和此仓库的示例实现。

使用方法

选项

  • media:用空格分隔的一个或多个媒体源 URL。
  • host(可选):与您在同一网络上的 Chromecast 设备的 IP 地址。
  • port(可选):Chromecast 设备的端口,默认为 8009

标志

  • –append (-a):是否将传入的媒体追加到当前播放列表中,而不是替换当前播放列表(如果重新连接成功)。默认为 false
  • –debug (-d):是否显示所有信息日志,默认为 false

使用方法

dart example/main.dart <media> [--host <host> [--port <port> [--append [ --debug]]]]

播放控制

在本示例中,您可以使用以下按键来控制视频的播放:

  • 空格键:切换暂停状态
  • S 键:停止播放
  • ESC 键:断开设备
  • 左箭头键:倒退 10 秒
  • 右箭头键:快进 10 秒

示例

dart example/main.dart http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4

重新连接到活动会话

如果您退出命令行而没有断开设备,视频将继续播放。要重新连接而不影响当前播放列表,只需运行命令而没有任何媒体 URL。例如:

dart example/main.dart --host=192.168.1.1

完整示例代码

以下是完整的示例代码,展示了如何使用 pure_chromecast 插件来控制 Chromecast 设备。

示例代码

import 'dart:convert';
import 'dart:math';

import 'package:args/args.dart';
import 'package:pure_chromecast/casting/cast.dart';
import 'package:pure_chromecast/utils/mdns_find_chromecast.dart' as find_chromecast;
import 'package:logging/logging.dart';
import 'package:universal_io/io.dart';

final Logger log = new Logger('Chromecast CLI');

void main(List<String> arguments) async {
  // 创建一个参数解析器,以便我们可以读取命令行的参数和选项
  final parser = new ArgParser()
    ..addOption('host', abbr: 'h', defaultsTo: '')
    ..addOption('port', abbr: 'p', defaultsTo: '8009')
    ..addOption('title', abbr: 't', defaultsTo: null)
    ..addOption('subtitle', abbr: 's', defaultsTo: null)
    ..addOption('image', abbr: 'i', defaultsTo: '')
    ..addFlag('append', abbr: 'a', defaultsTo: false)
    ..addFlag('debug', abbr: 'd', defaultsTo: false);

  final ArgResults argResults = parser.parse(arguments);

  if (true == argResults['debug']) {
    Logger.root.level = Level.ALL;
    Logger.root.onRecord.listen((LogRecord rec) {
      print('${rec.level.name}: ${rec.message}');
    });
  } else {
    Logger.root.level = Level.OFF;
  }

  String imageUrl = argResults['image'];
  final List<String> images = imageUrl != '' ? [imageUrl] : [];

  // 将每个剩余的参数字符串转换为 CastMedia 实例
  final List<CastMedia> media = argResults.rest
      .map((String i) => CastMedia(
          contentId: i,
          images: images,
          title: argResults['title'],
          subtitle: argResults['subtitle']))
      .toList();

  String host = argResults['host'];
  int? port = int.parse(argResults['port']);
  if ('' == host.trim()) {
    // 搜索!
    print('Looking for ChromeCast devices...');

    List<find_chromecast.CastDevice> devices =
        await find_chromecast.find_chromecasts();
    if (devices.length == 0) {
      print('No devices found!');
      return;
    }

    print("Found ${devices.length} devices:");
    for (int i = 0; i < devices.length; i++) {
      int index = i + 1;
      find_chromecast.CastDevice device = devices[i];
      print("$index: ${device.name}");
    }

    print("Pick a device (1-${devices.length}):");

    int? choice;

    while (choice == null || choice < 0 || choice > devices.length) {
      choice = int.parse(stdin.readLineSync()!);
      print(
          "Please pick a number (1-${devices.length}) or press return to search again");
    }

    find_chromecast.CastDevice pickedDevice = devices[choice - 1];

    host = pickedDevice.ip!;
    port = pickedDevice.port;

    print("Connecting to device: $host:$port");

    log.fine("Picked: $pickedDevice");
  }

  startCasting(media, host, port, argResults['append']);
}

void startCasting(
    List<CastMedia> media, String host, int? port, bool? append) async {
  log.fine('Start Casting');

  // 尝试加载以前保存为 JSON 的状态
  Map? savedState;
  try {
    File savedStateFile = File("./saved_cast_state.json");
    savedState = jsonDecode(await savedStateFile.readAsString());
  } catch (e) {
    // 尚未存在
    log.warning('error fetching saved state' + e.toString());
  }

  // 创建带有传入主机和端口的 Chromecast 设备
  final CastDevice device = CastDevice(
    host: host,
    port: port,
    type: '_googlecast._tcp',
  );

  // 实例化 Chromecast 发送者类
  final CastSender castSender = CastSender(
    device,
  );

  // 监听 Chromecast 会话更新并保存状态
  castSender.castSessionController.stream
      .listen((CastSession? castSession) async {
    if (castSession!.isConnected) {
      File savedStateFile = File('./saved_cast_state.json');
      Map map = {
        'time': DateTime.now().millisecondsSinceEpoch,
      }..addAll(castSession.toMap());
      await savedStateFile.writeAsString(jsonEncode(map));
      log.fine('Cast session was saved to saved_cast_state.json.');
    }
  });

  CastMediaStatus? prevMediaStatus;
  // 监听媒体状态更新,如暂停、播放、寻求等
  castSender.castMediaStatusController.stream
      .listen((CastMediaStatus? mediaStatus) {
    // 显示进度
    if (mediaStatus == null) {
      return;
    }
    if (null != prevMediaStatus && mediaStatus.volume != prevMediaStatus!.volume) {
      // 音量刚刚更新
      log.info('Volume just updated to ${mediaStatus.volume}');
    }
    if (null == prevMediaStatus || mediaStatus.position != prevMediaStatus?.position) {
      // 更新当前进度
      log.info('Media Position is ${mediaStatus.position}');
    }
    prevMediaStatus = mediaStatus;
  });

  bool connected = false;
  bool didReconnect = false;

  if (null != savedState) {
    // 如果我们有保存的状态,
    // 尝试重新连接
    connected = await castSender.reconnect(
      sourceId: savedState['sourceId'],
      destinationId: savedState['destinationId'],
    );
    if (connected) {
      didReconnect = true;
    }
  }

  // 如果重新连接失败或我们从未有过保存的状态
  // 连接到新的会话
  if (!connected) {
    connected = await castSender.connect();
  }

  if (!connected) {
    log.warning('COULD NOT CONNECT!');
    return;
  }
  log.info("Connected with device");

  if (!didReconnect) {
    // 不要在重新连接时启动,因为这会重置播放器状态
    castSender.launch();
  }

  // 加载 CastMedia 播放列表并发送到 Chromecast
  castSender.loadPlaylist(media, append: append);

  // 初始化键盘输入处理程序
  // 空格键 = 切换暂停
  // S 键 = 停止播放
  // 左箭头键 = 后退 10 秒
  // 右箭头键 = 快进 10 秒
  // 上箭头键 = 音量增加 5%
  // 下箭头键 = 音量减少 5%
  stdin.echoMode = false;
  stdin.lineMode = false;

  stdin.asBroadcastStream().listen((List<int> data) {
    _handleUserInput(castSender, data);
  });
}

void _handleUserInput(CastSender castSender, List<int> data) {
  if (data.length == 0) return;

  int keyCode = data.last;

  log.info("pressed key with key code: ${keyCode}");

  if (32 == keyCode) {
    // 空格键 = 切换暂停
    castSender.togglePause();
  } else if (115 == keyCode) {
    // S 键 = 停止
    castSender.stop();
  } else if (27 == keyCode) {
    // ESC 键 = 断开连接
    castSender.disconnect();
  } else if (65 == keyCode) {
    // 上箭头键
    double? volume = castSender.castSession?.castMediaStatus?.volume;
    if (volume != null) {
      castSender.setVolume(min(1, volume + 0.1));
    }
  } else if (66 == keyCode) {
    // 下箭头键
    double? volume = castSender.castSession?.castMediaStatus?.volume;
    if (volume != null) {
      castSender.setVolume(max(0, volume - 0.1));
    }
  } else if (67 == keyCode || 68 == keyCode) {
    // 左箭头键或右箭头键 = 后退 10 秒或快进 10 秒
    double seekBy = 67 == keyCode ? 10.0 : -10.0;
    if (null != castSender.castSession && null != castSender.castSession!.castMediaStatus) {
      castSender.seek(
        max(0.0, castSender.castSession!.castMediaStatus!.position! + seekBy),
      );
    }
  }
}

更多关于Flutter Chromecast控制插件pure_chromecast的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter Chromecast控制插件pure_chromecast的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何使用 pure_chromecast 插件在 Flutter 应用中控制 Chromecast 设备的代码示例。这个示例将展示如何初始化插件、搜索设备以及向设备发送媒体内容。

首先,确保你已经在 pubspec.yaml 文件中添加了 pure_chromecast 依赖:

dependencies:
  flutter:
    sdk: flutter
  pure_chromecast: ^x.y.z  # 请替换为最新版本号

然后,运行 flutter pub get 来安装依赖。

接下来,是主要的 Dart 代码示例:

import 'package:flutter/material.dart';
import 'package:pure_chromecast/pure_chromecast.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ChromecastManager? _chromecastManager;
  List<DiscoveredDevice> _discoveredDevices = [];
  DiscoveredDevice? _selectedDevice;

  @override
  void initState() {
    super.initState();
    initChromecastManager();
  }

  void initChromecastManager() async {
    _chromecastManager = await ChromecastManager.create();
    _chromecastManager!.startDeviceDiscovery();
    _chromecastManager!.deviceDiscoveryStateChanged.listen((state) {
      if (state == DeviceDiscoveryState.STOPPED) {
        // 自动停止发现时重新开始
        _chromecastManager!.startDeviceDiscovery();
      } else if (state == DeviceDiscoveryState.STARTED) {
        // 开始发现时,清空已发现的设备列表
        setState(() {
          _discoveredDevices = [];
        });
      }
    });
    _chromecastManager!.discoveredDevices.listen((devices) {
      setState(() {
        _discoveredDevices = devices;
      });
    });
  }

  void _selectDevice(DiscoveredDevice device) {
    setState(() {
      _selectedDevice = device;
    });
    // 连接并启动会话
    _chromecastManager!.connectToDevice(device).then((session) {
      session.loadMedia(
        MediaInfo(
          contentId: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
          contentType: 'video/mp4',
          metadata: MediaMetadata(
            title: 'Elephants Dream',
            subtitle: 'A short animation film',
            images: [
              Image(url: 'https://example.com/elephants-dream-poster.jpg'),
            ],
          ),
        ),
        autoplay: true,
      );
    }).catchError((error) {
      print('Error connecting to device: $error');
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Chromecast Control'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            children: [
              if (_discoveredDevices.isNotEmpty ...[
                Text('Discovered Devices:'),
                ListView.builder(
                  shrinkWrap: true,
                  itemCount: _discoveredDevices.length,
                  itemBuilder: (context, index) {
                    final device = _discoveredDevices[index];
                    return ListTile(
                      title: Text(device.friendlyName),
                      onTap: () => _selectDevice(device),
                    );
                  },
                ),
              ] else ...[
                Center(child: Text('No devices found.')),
              ],
            ],
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _chromecastManager?.stopDeviceDiscovery();
    super.dispose();
  }
}

代码解释:

  1. 依赖导入:导入 flutterpure_chromecast 包。
  2. 状态管理:使用 StatefulWidget 来管理应用状态。
  3. 初始化 Chromecast 管理器:在 initState 方法中初始化 ChromecastManager 并开始设备发现。
  4. 监听设备发现状态:使用 deviceDiscoveryStateChanged 监听设备发现状态的变化,并在发现停止时重新启动发现。
  5. 监听已发现的设备:使用 discoveredDevices 监听已发现的设备列表,并在列表更新时调用 setState 更新 UI。
  6. 选择设备:定义一个 _selectDevice 方法来选择设备并尝试连接,连接成功后加载媒体内容。
  7. UI 构建:使用 ListView.builder 显示已发现的设备列表,并为每个设备项设置点击事件。
  8. 资源释放:在 dispose 方法中停止设备发现以释放资源。

请注意,实际使用中需要处理更多的错误情况和边界情况,例如设备连接失败、媒体加载失败等。这个示例代码提供了一个基本的框架,你可以根据需要进行扩展和修改。

回到顶部