用flutter 写一个电视应用
一,场景家里买了一台小米电视,回去打开一看,就是一个安卓系统,内置的节目除了广告就是让买会员。我就想简简单单的看个电视,不想看广告,因为之前用qt+ubuntu做过电视盒子,那为何不参照以往的思路做个电视app呢。二,原型功能要足够简单,就是要找到当年看熊猫牌 电视的感觉,进入后首先就是默认的央视1,菜单可打开列表选择频道,遥控器上下键默认切换频道。三,实现思路1,频道列表 开始考虑的是保存到本地
一,场景
家里买了一台小米电视,回去打开一看,就是一个安卓系统,内置的节目除了广告就是让买会员。我就想简简单单的看个电视,不想看广告,因为之前用qt+ubuntu做过电视盒子,那为何不参照以往的思路做个电视app呢。
二,原型
功能要足够简单,就是要找到当年看熊猫牌 电视的感觉,进入后首先就是默认的央视1,菜单可打开列表选择频道,遥控器上下键默认切换频道。
三,实现思路
1,频道列表 开始考虑的是保存到本地json文件中,但是问题是 这种直播的地址有可能会更换,并且如果自己想再加入其它频道又要重新打包,因为将json文件放在云端服务器上,软件每次启动去读取这文件 更方便。
所以 用go语言写了一个get接口,读取json文件并返回。
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
)
type Address struct {
Name string `json:"name"`
Url string `json:"url"`
}
type TVlist struct {
TVList []Address
}
func tvlist(w http.ResponseWriter, r *http.Request) {
body, _ := ioutil.ReadAll(r.Body)
fmt.Println(string(body))
//读取json文件
jsonFile, err := os.Open("tvlist.json")
if err != nil {
fmt.Println(err)
}
defer jsonFile.Close()
byteValue, _ := ioutil.ReadAll(jsonFile)
fmt.Println(string(byteValue))
var result map[string]interface{}
json.Unmarshal([]byte(byteValue), &result)
fmt.Println(result)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
json, _ := json.Marshal(result)
w.Write(json)
}
func main() {
http.HandleFunc("/tvlist", tvlist)
err := http.ListenAndServe(":8000", nil)
if err != nil {
log.Fatal("ListenAndServdr:", err.Error())
}
}
软件第一步就是调用这个接口,返回电视列表。
{
"list": [
{
"name": "CCTV1",
"url": "http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8"
},
{
"name": "CCTV2",
"url": "http://ivi.bupt.edu.cn/hls/cctv2hd.m3u8"
},
{
"name": "CCTV3",
"url": "http://ivi.bupt.edu.cn/hls/cctv3hd.m3u8"
},
{
"name": "CCTV4",
"url": "http://ivi.bupt.edu.cn/hls/cctv4hd.m3u8"
},
{
"name": "CCTV5",
"url": "http://cctv5ksh5ca.v.kcdnvip.com/live/cctv5_2/index.m3u8"
},
{
"name": "CCTV6",
"url": "http://ivi.bupt.edu.cn/hls/cctv6hd.m3u8"
},
{
"name": "CCTV7",
"url": "http://ivi.bupt.edu.cn/hls/cctv7hd.m3u8"
},
{
"name": "CCTV8",
"url": "http://ivi.bupt.edu.cn/hls/cctv8hd.m3u8"
},
{
"name": "CCTV9",
"url": "http://ivi.bupt.edu.cn/hls/cctv9hd.m3u8"
},
{
"name": "CCTV10",
"url": "http://ivi.bupt.edu.cn/hls/cctv10hd.m3u8"
},
{
"name": "CCTV11",
"url": "http://ivi.bupt.edu.cn/hls/cctv11.m3u8"
},
{
"name": "CCTV12",
"url": "http://ivi.bupt.edu.cn/hls/cctv12hd.m3u8"
},
{
"name": "CCTV13",
"url": "http://ivi.bupt.edu.cn/hls/cctv13hd.m3u8"
},
{
"name": "CCTV14",
"url": "http://ivi.bupt.edu.cn/hls/cctv14hd.m3u8"
},
{
"name": "CCTV15",
"url": "http://ivi.bupt.edu.cn/hls/cctv15hd.m3u8"
},
{
"name": "湖南卫视",
"url": "http://ivi.bupt.edu.cn/hls/hunanhd.m3u8"
},
{
"name": "安徽卫视",
"url": "http://cctvtxyh5ca.liveplay.myqcloud.com/wstv/anhui_2_hd/index.m3u8"
},
{
"name": "东方卫视",
"url": "http://cctvtxyh5ca.liveplay.myqcloud.com/wstv/dongfang_2_hd/index.m3u8"
},
{
"name": "江苏卫视",
"url": "http://mgzb.live.miguvideo.com:8088/envivo_x/2018/SD/jiangsuTV/350/index.m3u8?msisdn=migu&mdspid=&spid=699067&netType=0&sid=5500199481&pid=2028597139×tamp=20200905170351&Channel_ID=0116_25000000-99000-100300010010001&ProgramID=623899540&ParentNodeID=-99&assertID=5500199481&client_ip=125.34.14.115&SecurityKey=20200905170351&mvid=&mcid=&mpid=&playurlVersion=SJ-A1-0.0.3&userid=&jmhm=&videocodec=h264&encrypt=dbdf3a2df384955fbb5e9a009334da0c"
},
{
"name": "浙江卫视",
"url": "http://hw-m-l.cztv.com/channels/lantian/channel01/360p.m3u8"
},
{
"name": "辽宁卫视",
"url": "http://cctvtxyh5ca.liveplay.myqcloud.com/wstv/liaoning_2_hd/index.m3u8"
},
{
"name": "广西卫视",
"url": "http://cctvtxyh5ca.liveplay.myqcloud.com/wstv/guangxi_2_hd/index.m3u8"
},
{
"name": "北京卫视",
"url": "http://cctvtxyh5ca.liveplay.myqcloud.com/wstv/btv1_2_hd/index.m3u8"
},
{
"name": "广东卫视",
"url": "http://cctvtxyh5ca.liveplay.myqcloud.com/wstv/guangdong_2_hd/index.m3u8"
},
{
"name": "江西卫视",
"url": "http://cctvtxyh5ca.liveplay.myqcloud.com/wstv/jiangxi_2_hd/index.m3u8"
},
{
"name": "四川卫视",
"url": "http://mgzb.live.miguvideo.com:8088/envivo_v/2018/SD/sichuanTV/350/index.m3u8?msisdn=migu&mdspid=&spid=800033&netType=0&sid=5500321161&pid=2028597139×tamp=20200905174519&Channel_ID=0116_25000000-99000-100300010010001&ProgramID=630288361&ParentNodeID=-99&assertID=5500321161&client_ip=125.34.14.115&SecurityKey=20200905174519&mvid=&mcid=&mpid=&playurlVersion=SJ-A1-0.0.3&userid=&jmhm=&videocodec=h264&encrypt=70f358eefb8d868ad7a8d9653c0c3538"
},
{
"name": "山东卫视",
"url": "http://cctvtxyh5ca.liveplay.myqcloud.com/wstv/shandong_2_hd/index.m3u8"
},
{
"name": "天津卫视",
"url": "http://cctvtxyh5ca.liveplay.myqcloud.com/wstv/tianjin_2_hd/index.m3u8"
},
{
"name": "深圳卫视",
"url": "http://cctvtxyh5ca.liveplay.myqcloud.com/wstv/shenzhen_2_hd/index.m3u8"
},
{
"name": "云南卫视",
"url": "http://cctvtxyh5ca.liveplay.myqcloud.com/wstv/yunnan_2_hd.m3u8"
},
{
"name": "河南卫视",
"url": "http://cctvtxyh5ca.liveplay.myqcloud.com/wstv/henan_2_hd.m3u8"
},
{
"name": "香港卫视",
"url": "http://zhibo.hkstv.tv/livestream/mutfysrq/playlist.m3u8"
},
{
"name": "北京影视",
"url": "http://cctvtxyh5ca.liveplay.myqcloud.com/wstv/btv4_2_hd.m3u8"
},
{
"name": "浙江影视",
"url": "http://hw-m-l.cztv.com/channels/lantian/channel05/360p.m3u8"
},
{
"name": "东方影视",
"url": "http://mgzb.live.miguvideo.com:8088/wd_r4/dfl/dianshijusd/350/index.m3u8?msisdn=migu&mdspid=&spid=699001&netType=0&sid=5500013485&pid=2028597139×tamp=20200905175356&Channel_ID=0116_25000000-99000-100300010010001&ProgramID=618954718&ParentNodeID=-99&assertID=5500013485&client_ip=125.34.14.115&SecurityKey=20200905175356&mvid=&mcid=&mpid=&playurlVersion=SJ-A1-0.0.3&userid=&jmhm=&videocodec=h264&encrypt=7a4fbf62b17921bb018f548069f1210a"
},
{
"name": "江西影视",
"url": "http://live.jxtvcn.com.cn/live-jxtv/tv_jxtv4.m3u8?token=3475ad84bd174a0bc7da2b31f4bcb90c&t=1599299662"
},
{
"name": "安徽影视",
"url": "http://zbbf2.ahtv.cn/live/756.m3u8"
}
]
}
2,安卓应用 使用flutter,因为原生的不会。这里就比较简单了,就是一个播放器根据选择的频道播放不同地址。
播放器使用 vlc插件,然后一个gridview,之后处理不用的按键,播放或者打开列表即可。
import 'package:flutter/material.dart';
import 'package:flutter_vlc_player/flutter_vlc_player.dart';
import 'package:flutter_tv/auto_focus.dart';
import 'package:dio/dio.dart';
import 'dart:convert';
import 'dart:io';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
);
}
}
class TVUrl {
final String name;
final String url;
TVUrl(this.name, this.url);
TVUrl.fromJson(Map<String, dynamic> json)
: name = json['name'],
url = json['url'];
Map<String, dynamic> toJson() =>
{
'name': name,
'url': url,
};
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
VlcPlayerController _videoPlayerController;
bool _menuVisible = true; //是否显示 频道列表
var _addressMap = <String, String>{}; //名称 地址集合
var _curAddress = "http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8"; //当前地址
Future<void> initializePlayer() async {}
void getAddressList() async{
var url = 'http://xxxxxxx:8000/tvlist';
Dio _dio = Dio();
String result;
try {
Response response = await _dio.get(url);//2
if (response.statusCode == HttpStatus.ok) {
var data= jsonDecode(response.toString());//3
var listData = data["list"];
List urlList = listData.map((m)=>new TVUrl.fromJson(m)).toList();
_addressMap.clear();
for(int i=0;i<urlList.length;i++){
TVUrl tvUrl = urlList[i];
_addressMap[tvUrl.name]=tvUrl.url;
}
setState(() {
});
} else {
result =
'Error getAddressList status ${response.statusCode}';
print(result);
}
} catch (exception) {
result =exception.toString();
print(result);
}
}
List<String> getDataList() {
return _addressMap.keys.toList();
}
List<Widget> getWidgetList() {
return getDataList().map((item) => getItemContainer(item)).toList();
}
Widget getItemContainer(String item) {
return AutoFocus(
child: Card(
color: Color.fromARGB(0, 0, 0, 0),
child: InkWell(
splashColor: Colors.blue.withAlpha(30),
onTap: () async {
_curAddress = _addressMap[item];
setState(() {
});
await _videoPlayerController.setMediaFromNetwork(
_curAddress,
hwAcc: HwAcc.FULL,
);
print('Card tapped.'+_curAddress);
},
child: Container(
color: Color.fromARGB(10, 0, 0, 0),
//width: 100,
//height: 50,
child: Center(
child: Text(item,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 25))),
),
),
),
//列表显示 按下确定 换台
onEnter: ()async {
if (_menuVisible) {
print('Card enter.' + _addressMap[item]);
_curAddress = _addressMap[item];
await _videoPlayerController.setMediaFromNetwork(
_curAddress,
hwAcc: HwAcc.FULL,
);
}
},
//按下菜单
onMenu: () {
print('Card menu');
_menuVisible = !_menuVisible;
setState(() {});
},
//列表隐藏时 选中当前频道 自动换台
onFocused: () async{
if (!_menuVisible){
print('Card focused.' + _addressMap[item]);
_curAddress = _addressMap[item];
await _videoPlayerController.setMediaFromNetwork(
_curAddress,
hwAcc: HwAcc.FULL,
);
}
},
);
}
@override
void initState() {
super.initState();
_videoPlayerController = VlcPlayerController.network(
_curAddress,
hwAcc: HwAcc.FULL,
autoPlay: true,
options: VlcPlayerOptions(),
);
_addressMap["CCTV-1"] = 'http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8';
_addressMap["CCTV-2"] = "http://ivi.bupt.edu.cn/hls/cctv2hd.m3u8";
_addressMap["湖南卫视"] = "http://ivi.bupt.edu.cn/hls/hunanhd.m3u8";
//获取地址
getAddressList();
}
@override
void dispose() async {
super.dispose();
await _videoPlayerController.stopRendererScanning();
await _videoPlayerController.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(),
body: Center(
child: Stack(
alignment: AlignmentDirectional.bottomStart,
children: <Widget>[
VlcPlayer(
controller: _videoPlayerController,
aspectRatio: 16 / 9,
placeholder: Center(child: CircularProgressIndicator()),
),
AnimatedOpacity(
duration: Duration(milliseconds: 300),
opacity: _menuVisible ? 1.0 : 0.0,
child: AutoFocusContainer(
child: Container(
decoration: BoxDecoration(color: Color(0x90000033)),
margin:EdgeInsets.only( top: 0),
child: GridView.count(
//水平子Widget之间间距
crossAxisSpacing: 15.0,
//垂直子Widget之间间距
mainAxisSpacing: 20.0,
//GridView内边距
padding: EdgeInsets.all(10.0),
//一行的Widget数量
crossAxisCount: 6,
//子Widget宽高比例
childAspectRatio: 2.0,
//子Widget列表
children: getWidgetList(),
),
),
),
),
],
),
));
}
}
三,最终效果(模拟器很卡)
打包release :https://segmentfault.com/a/1190000021827419
release无法上网:https://www.cnblogs.com/joe235/p/11492273.html
更多推荐
所有评论(0)