Skip to content

Yu2002s/DongYuTVWeb

Repository files navigation

DongYuTVWeb

轻松在电视上观看直播,操作简单,适合家中老人使用,使用 WebView JavaScript 注入方式实现

操作

  • 方向上: 上一个频道
  • 方向下: 下一个频道
  • 左键: 声音减
  • 右键: 声音加
  • 确定键: 播放/暂停
  • 数字键: 换台
  • 菜单键:选择换台
  • 返回键:返回/退出
  • #号键:强制刷新当前频道(有问题时使用)

使用老式的闭路电视操作,适合老人使用!

安装

  1. USB数据线连接电视和手机(或U盘),打开手机的通知,将仅充电改为文件传输,然后把安装包复制到电视,电视找到相关USB存储,安装apk
  2. 通过无线方式安装,需要 android 11 以上,打开电视 adb调试(如何开启请百度),使用甲壳虫adb助手连接电视后安装

具体请百度或问AI,这里不进行详细描述

安装事项

有些电视限制安装第三方App,需要使用 adb 禁用软件包安装程序

tcl电视举例(其他品牌电视请问AI或百度):需要电视开启adb,进入 设置 -> 关于 -> 依次按下 上下左右 ,之后会显示扩展设置, 找到开启adb开关,然后使用 https://www.jiakechong.net/ 甲壳虫adb助手 连接到电视, 连接之前需要获取电视在网络中的ip,打开电视网络信息会有ipv4 地址,这个就是。然后用甲壳虫找到应用管理, 禁用软件安装包程序就可以安装app了。 安装完成之后再把软件安装包程序启用

开机自启动

  1. 查找电视的一些设置、App,看看有没有应用自启动相关的设置,对本应用开启自启动。可百度或 Ai 搜索自启动相关文章
  2. 确保 App 已经打开过,并允许了悬浮窗权限
  3. 设置默认的桌面,一般按 Home键(主页键) 会提示设置默认桌面,可以设置为本App并选择始终,或查找电视相关设置和App设置默认桌面

不保证所有电视都支持,仅在原生系统虚拟机 Android 12 测试通过

如果上面的方法不奏效,可以尝试以下方法:

  1. 进入电视的工厂模式
  2. 开启 USB 调试
  3. 使用无线调试或数据线连接电视和电脑(手机-需要安装终端)电脑安装adb 手机可以安装MT管理器,自带终端
  4. 进入终端,输入以下命令
# 无线连接 adb connect ip:端口
adb devices # 查看设备是否连接成功
adb shell # 进入管理员模式
cmd package set-home-activity "xyz.jdynb.tv/.MainActivity" # 设置默认的启动项
reboot # 重启设备

注意:定制系统可能无效

X5内核

部分电视系统内核的版本低,所以需要使用X5内核,这个需要自己自行编译此项目,需要一定的android基础。

  1. 腾讯浏览服务 登录账号,之后创建应用,上传打包后的 apk
  2. 进行实名认证
  3. 在应用列表点击查看应用,打开版本配置列表
  4. 获取配置文件,将下载的配置文件,替换 app/src/x5/assets/tbs/config.tbs 文件
  5. 修改 app/src/main/build.gradle 文件,编辑 signingConfigs 签名配置, 选择构建分支为 x5,重新进行打包构建 apk
  6. 运行安装完成后,查看是否成功

接入文档

X5网页引擎(公网版)接入文档

自定义频道

频道完整配置文件 main/assets/live-3.jsonc live-3.jsonc

最新调整:

  • 去除 number 频道号码
  • 变更 live.jsonclive-3.jsonc

1. 对于没有验证的直播地址

修改 channel 频道列表

{
    // ...
    "channel": [
        // ...
        {
            "channelType": "xxx", // 频道类型(分类名称)
            "player": "common", // 播放器 id,这里写上,下方的 channelList 中没有设置的就默认为这个值
            "channelList": [
              {
                "channelName": "香港卫视", // 频道名称 (需要唯一)
                "tvLogo": "", // logo 目前没用到,可以不写
                "number": 6, // 填写唯一的频道号码,用于数字换台 (废弃)
                "player": "common", // 播放器 id,这个上方已经内置了,只需要写 common
                "args": { // 播放参数,固定 liveUrl 即可
                  "liveUrl": "https://wwww.tv.com/playlist.m3u8?"
                }
              }
            ]
        }
    ]
}

注意上方的 liveUrl 只能是普通的 m3u8地址,并且没有带时间戳等相关的参数

2. 对于开发人员

有些直播地址是有验证参数的,需要在脚本中进行处理生成

增加自定义频道,修改 channel 配置

// 省略其他
{
  // ...
  "channel": [
    // ...
    {
      "channelType": "xx电视分类",
      "player": "custom", // 设置自定义播放 id
      "channelList": [
        // ...
        {
          "channelName": "xx电视",
          "tvLogo": "https://web.cmc.hebtv.com/cms/rmt0336/upload/Image/9/9tpklm/2025/07/18/30aaea69a1de46a881cef5d759d30b3e.png",
          "number": 23
        }
        // ...
      ]
    }
  ]
}

修改 player 配置

{
  "player": {
    "id": "custom", // 播放器 id,和上面定义的频道 player 参数一致
    "name": "simple", // 内置的播放器名称,做了特殊处理,默认写这个
    "script": { // 需要执行的 js 脚本
      "init": [ // 在网页初始化完成时候执行的 js 脚本
        "https://gitee.com/jdy2002/DongYuTvWeb/raw/master/app/src/main/assets/js/hebei/init.js"
      ],
      "play": [ // 响应播放事件执行的 js 脚本
        "https://gitee.com/jdy2002/DongYuTvWeb/raw/master/app/src/main/assets/js/hebei/play.js"
      ],
      "resume_pause": [] // 暂停恢复执行的 js 脚本,这里留空,因为内部已经做了处理了
    }
  }
}

编辑 js 脚本

不需要云服务器,在 gitee 上建一个仓库,上面脚本地址自己写上仓库中实际的地址即可,具体格式参照上面的

simple 播放器已经内置了 CryptoJS,所以这里就不需要引入了,直接使用即可

脚本中,{{channelName}} 会被替换成用户选择的频道名称,当然其他 args 参数也会被这样替换进入

内部已经关闭了跨域限制,可以直接请求

playLive 是内置的方法,可以直接调用传入地址进行播放

注意:使用fetch请求时,有些会检测Referer请求头,请设置X-Referer请求头,内部会进行处理, 请求头数据请使用 X-Body 添加到请求头中,这里的数据将作为 Body 发送,

init.js, 对应上面 init 执行的脚本

通过普通 fetch 发送请求

这种方式有限制,有些请求会检测 Referer 请求头,请设置自定义请求头 X-Referer, 推荐使用内置的 HttpUtil 发送请求

(function() {
    // 这里在播放地址上面加上需要的参数
    window.addLiveUrlQuery = function (obj) {
       let ts = parseInt(new Date().getTime() / (1000)) + (7200);
       // CryptoJS 已经是内置的,可以直接使用
       return obj.liveVideo[0].formats[0].url + "?t=" + ts + '&k=' + CryptoJS.MD5(obj.appCustomParams.movie.liveUri + obj.appCustomParams.movie.liveKey + ts);;
    }

    fetch("https://api.cmc.hebtv.com/scms/api/com/article/getArticleList?catalogId=32557&siteId=1", {
      "headers": {
        "accept": "application/json, text/javascript, */*; q=0.01",
        "accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
        "sec-ch-ua": "\"Google Chrome\";v=\"143\", \"Chromium\";v=\"143\", \"Not A(Brand\";v=\"24\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"Windows\"",
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "same-site",
        // 如果不检测 Referer的话,可以正常写
        "X-Referer": "https://www.hebtv.com/" // X-Referer 会自动在请求头添加 Referer,防止被检测
        // 必须在设置了 X-Referer 之后,X-Body 才有效
        "X-Body": "" // 这里设置请求体数据
      },
      "body": null,
      "method": "GET"
    }).then(res => {
      return res.json()
    }).then(res => {
        const news = res.returnData.news
        window.channelList = news.map(item => {
            return {
                title: item.title,
                liveVideo: item.liveVideo,
                appCustomParams: item.appCustomParams
            }
        })

        // {{channelName}} 会被替换成频道名称,上面的 channel中的属性都会被传入到脚本中,这里可以进行自定义处理
        const channelItem = window.channelList.find(item => item.title === '{{channelName}}')
        const playUrl = window.addLiveUrlQuery(channelItem)
        // 初始化时机执行的,所以这里需要对当前播放的频道进行播放,play.js 不会在初始化时候调用生效
        playLive(playUrl)
    })
})();
使用 HttpUtil 发送请求

内置在 simple 播放器中的,可以直接使用,推荐使用这种方式发送请求

const response = await HttpUtil.post(`https://feiying.litenews.cn/api/v1/auth/exchange?t=${now}&s=${s}`, body, {
    headers: {
        "accept": "*",
        "accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
        "content-type": "text/plain",
        "priority": "u=1, i",
        "sec-ch-ua": "\"Google Chrome\";v=\"143\", \"Chromium\";v=\"143\", \"Not A(Brand\";v=\"24\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"Windows\"",
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "cross-site",
        "Referer": "https://v.iqilu.com/"
    },
    responseType: 'text'
})
const result = JSON.parse(decrypt(response.data))
playLive(result.data)

HttpUtil 的定义参见 HttpUtil

play.js 处理播放

(function() {
    const channelItem = window.channelList.find(item => item.title === '{{channelName}}')
    const playUrl = window.addLiveUrlQuery(channelItem)
    playLive(playUrl)
})();

Base 播放器

对于加密的直播地址,需要使用 Base 播放器,直接通过浏览器加载来实现播放,去除播放器以外的节点,实现全屏播放。

init.js实现 全屏并去除其他无用样式进行初始化

play.js实现 直接使用 window.location.href 进行跳转

resume_pause.js实现 查找按钮并点击

配置参考

url 参数支持动态参数,使用 {{}} 方式传入

// player 配置
{
  "id": "guangxi",
  "name": "base", // 指定为 base
  "url": "https://tv.gxtv.cn/channel/channelivePlay_{{id}}.html", // url 支持动态参数
  // 设置指定的脚本
  "script": {
    "init": ["https://gitee.com/jdy2002/DongYuTvWeb/raw/master/app/src/main/assets/js/guangxi/init.js"],
    "play": ["https://gitee.com/jdy2002/DongYuTvWeb/raw/master/app/src/main/assets/js/guangxi/play.js"],
    "resume_pause": ["https://gitee.com/jdy2002/DongYuTvWeb/raw/master/app/src/main/assets/js/guangxi/resume_pause.js"]
  }
}

id 参数会注入到上面 url 的 {{id}} 中

// channel 配置
{
  "channelType": "广西",
  "player": "guangxi",
  "channelList": [
    {
      "channelName": "广西卫视",
      "args": {
        "id": "e7a7ab7df9fe11e88bcfe41f13b60c62"
      }
    },
    {
      "channelName": "综艺旅游",
      "args": {
        "id": "f3335975f9fe11e88bcfe41f13b60c62"
      }
    }
  ]
}

simple 播放器内置的 html 代码

/main/assets/html/simple_player.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Document</title>
    <style>
        body {
            background: #000000;
        }

        #video {
            width: 100vw;
            height: 100vh;
        }

        #loading {
            position: fixed;
            left: 0;
            top: 0;
            z-index: 99;
            width: 100vw;
            height: 100vh;
            font-size: 30px;
            font-weight: bold;
            color: white;
            display:flex;
            align-items: center;
            justify-content: center;
        }
    </style>
    <!-- 这里内部做了处理 -->
    <script src="../js/lib/dy-http-util.js"></script>
    <script src="../js/lib/dy-crypto-js.min.js"></script>
    <script src="../js/lib/dy-hls.min.js"></script>
</head>
<body>
<video autoplay id="video"></video>
<div class="loading-overlay" id="loading">
    加载中...请稍后
</div>
</body>

<script>
    const video = document.querySelector("#video")

    video.addEventListener('loadeddata', function() {
        hideLoading()
    })

    function hideLoading() {
        const loading = document.getElementById('loading');
        loading.style.display = 'none';
    }

    function resumeOrPause() {
        if (video.paused) {
            video.play();
        } else {
            video.pause();
        }
    }

    let hls = null;

    function playLive(url, headers) {
        video.volume = 1
        
        // 检查是否支持 HLS.js 且为 m3u8 文件
        if (Hls.isSupported() && url.includes('.m3u8')) {
            if (hls) {
                hls.destroy();
            }
            
            hls = new Hls({
                debug: false,
                xhrSetup: function(xhr, url) {
                    // xhr.setRequestHeader('X-Referer', headers['Referer'])
                    // 这里设置请求头
                    if (headers) {
                        for (const key in headers) {
                            xhr.setRequestHeader(key, headers[key]);
                        }
                    }
                }
            });
            
            hls.loadSource(url);
            hls.attachMedia(video);
            
            hls.on(Hls.Events.MANIFEST_PARSED, function() {
                video.play();
            });
            
            hls.on(Hls.Events.ERROR, function(event, data) {
                console.error('HLS Error:' + JSON.stringify(data));
                if (data.fatal) {
                    switch(data.type) {
                        case Hls.ErrorTypes.NETWORK_ERROR:
                            console.log('Network error, trying to recover');
                            hls.startLoad();
                            break;
                        case Hls.ErrorTypes.MEDIA_ERROR:
                            console.log('Media error, trying to recover');
                            hls.recoverMediaError();
                            break;
                        default:
                            console.log('Fatal error, destroying HLS instance');
                            hls.destroy();
                            break;
                    }
                }
            });
        } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
            // iOS Safari 原生支持
            video.src = url;
            video.play();
        } else {
            // 其他格式直接播放
            video.src = url;
            video.play();
        }
    }
</script>
</html>

其他使用方法,请查看项目的 /app/src/main/assets 中的代码

截图

截图

截图

截图

注意

仅供学习交流,请勿用于商业用途