首页 > 技术文章 > YY播放器源码解析

kozo4 2021-11-14 20:54 原文

YY播放器使用Flutter编写的一个聚合播放器, 起因是看了 ZY-Player的源码, 发现实现挺有意思的, 也比较简单

地址: https://github.com/waifu-project/movie

下载源码之后, 首先从入口函数入手

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await XHttp.init();
  await GetStorage.init();
  await MirrorManage.init();
  final localStorage = GetStorage();

  bool isDark = (localStorage.read(ConstDart.ls_isDark) ?? false);
  bool systemBrightnessFlag = (localStorage.read(ConstDart.auto_dark) ?? false);

  Brightness wrapperIfDark = Brightness.light;

  {
    if (isDark) wrapperIfDark = Brightness.dark;
    if (GetPlatform.isWindows && systemBrightnessFlag) {
      var windowMode = getWindowsThemeMode();
      wrapperIfDark = windowMode;
    }

  if (GetPlatform.isDesktop) {
    doWhenWindowReady(() {
      final initialSize = Size(990, 720);
      appWindow.minSize = initialSize;
      appWindow.size = initialSize;
      appWindow.alignment = Alignment.center;
      appWindow.show();
    });
  }
}

大概可以看出支持了 windows 平台(从 doWhenWindowReady 回调可以看出)

继续从依赖中猜一下大概实现方式

dependencies: 
  cupertino_icons: ^1.0.2 # 图标
  get: 4.3.8 # 状态管理
  salomon_bottom_bar: ^3.1.0
  dio: ^4.0.0 # 网络库
  cookie_jar: ^3.0.1
  dio_cookie_manager: ^2.0.0
  html: ^0.15.0
  flutter_cupertino_settings: ^0.5.0
  webview_flutter: ^2.1.1 # webview组件
  flappy_search_bar_ns: ^2.0.2
  xml2json: ^5.3.1 # xml to json
  modal_bottom_sheet: ^2.0.0
  pull_to_refresh: ^2.0.0
  cupertino_list_tile: ^0.2.0
  cached_network_image: ^3.1.0
  chewie: ^1.2.2 # 视频播放
  video_player: ^2.2.5
  get_storage: ^2.0.3
  path_provider: ^2.0.6
  flutter_html: ^2.1.5
  clipboard: ^0.1.3
  wakelock: ^0.5.6
  auto_orientation: ^2.1.0
  desktop_webview_window: ^0.0.5
  bitsdojo_window: ^0.1.1+1
  url_launcher: ^6.0.12

首先是所谓的聚合实现
lib/impl/movie.dart中有一个抽象类

abstract class MovieImpl {

  /// 源信息
  MovieMetaData get meta;

  /// 获取首页
  Future<List<MirrorOnceItemSerialize>> getHome({
    int page = 1,
    int limit = 10,
  });

  /// 搜索
  Future<List<MirrorOnceItemSerialize>> getSearch({
    required String keyword,
    int page = 1,
    int limit = 10,
  });

  /// 获取视频详情
  Future<MirrorOnceItemSerialize> getDetail(String movie_id);
}

基本上源的操作就那几个:

  • 首页
  • 搜索
  • 详情

接下来在看一下 MirrorOnceItemSerialize, 就是定义的一个标准实体类

class MirrorOnceItemSerialize {
  /// id
  final String id;
  /// 标题
  final String title;
  /// 介绍
  final String desc;
  /// 喜欢
  final int likeCount;
  /// 访问人数
  final int viewCount;
  /// 不喜欢
  final int dislikeCount;
  /// 小封面图(必须要有)
  final String smallCoverImage;
  /// 大封面图
  final String bigCoverImage;
  /// 视频列表
  final List<MirrorSerializeVideoInfo> videos;
  /// 视频信息
  /// 视频尺寸大小
  /// 视频长度大小
  final MirrorSerializeVideoSize videoInfo;
}

接下来就是实现这个抽象类, 比如内置的奈菲源, 来看一下其的实现方式

@override
  Future<MirrorOnceItemSerialize> getDetail(String movie_id) async {
    var detailURL = createURL(path: "/detail/" + movie_id);
    var resp = await XHttp.dio.get(
      detailURL,
      options: Options(
        headers: header,
      ),
    );
    var parse = html.parse(resp.data);
    var ele = parse.querySelector(".myui-panel_hd");
    if (ele == null) throw UnimplementedError();
    var mirrorList = ele.querySelectorAll('li');
    List<fetchMovieFrameURL> frames = [];
    mirrorList.map((e) {
      // ...(忽略代码)
      return bat;
    }).toList();
    var data = await Future.wait<MirrorSerializeVideoInfo>(frames.map(
      (e) async {
        var url = await findIframeM3u8URL(e.id);
        var title = e.title;
        var item = MirrorSerializeVideoInfo(
          url: url,
          type: KBaseMirrorMovie.easyGetVideoType(url),
          name: title,
        );
        return item;
      },
    ).toList());
    var infoEle = parse.querySelector(
      '.myui-vodlist__thumb.img-md-220.img-xs-130.picture',
    );
    var coverImage = infoEle!.querySelector("img")?.attributes['data-original'] ?? "";
    var title = infoEle.attributes['title'] ?? "";
    return MirrorOnceItemSerialize(
      id: movie_id,
      smallCoverImage: coverImage,
      title: title,
      videos: data,
    );
  }

相信同学们基本上看明白了: 这个奈菲源的实现是通过解析 html 然后拿到数据
所以说, 只要你实现了视频源抽象类, 你想怎么玩就怎么玩(所以快来PR添加资源吧)

同样的, 根据这个抽象类, 实现了 ZY-Player 的源, 单个资源数据类型

{
    "key": "快播云",
    "id": 1,
    "name": "快播云",
    "api": "http://www.kuaibozy.com/api.php/provide/vod/from/kbm3u8/at/xml/",
    "download": "",
    "jiexiUrl": "https://jx.7kjx.com/?url=",
    "group": "默认",
    "isActive": true,
    "status": "可用",
    "reverseOrder": true
}

继续扒一下 ZF-Player的源码: https://github.com/cuiocean/ZY-Player/blob/master/src/lib/site/tools.js

就可以找到规则:

首页就是: $ROOT?ac=videolist&pg=${pg}

  /**
   * 获取资源列表
   * @param {*} key 资源网 key
   * @param {number} [pg=1] 翻页 page
   * @param {*} t 分类 type
   * @returns
   */
  list (key, pg = 1, t) {
    return new Promise((resolve, reject) => {
      this.getSite(key).then(res => {
        const site = res
        let url = null
        if (t) {
          url = `${site.api}?ac=videolist&t=${t}&pg=${pg}`
        } else {
          url = `${site.api}?ac=videolist&pg=${pg}`
        }
        // todo
      })
    })
  },

详情就是: $ROOT?ac=videolist&ids=${id}

/**
   * 获取资源详情
   * @param {*} key 资源网 key
   * @param {*} id 资源唯一标识符 id
   * @returns
   */
  detail (key, id) {
    return new Promise((resolve, reject) => {
      this.getSite(key).then(res => {
        const url = `${res.api}?ac=videolist&ids=${id}`
        // TODO
    })
  }

搜索就是: $ROOT?wd=${wd}

/**
   * 搜索资源
   * @param {*} key 资源网 key
   * @param {*} wd 搜索关键字
   * @returns
   */
  search (key, wd) {
    return new Promise((resolve, reject) => {
      this.getSite(key).then(res => {
        const site = res
        const url = `${site.api}?wd=${encodeURI(wd)}`
        // TODO
      })
    })
  }

根据此规则就可以兼容 ZY-Player 的资源

后续

大概实现方式这样, 比较简单, 后续加一点点细节就可以了

有兴趣的同学可以看一下源码: https://github.com/waifu-project/movie

mail: chenhonzhou@gmail.com

推荐阅读