一,场景

家里买了一台小米电视,回去打开一看,就是一个安卓系统,内置的节目除了广告就是让买会员。我就想简简单单的看个电视,不想看广告,因为之前用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

Logo

智屏生态联盟致力于大屏生态发展,利用大屏快应用技术降低开发者开发、发布大屏应用门槛

更多推荐