无人直播推流——开发汇总

Flutter 撸一个无人直播推流项目

运用技术:

·Flutter编写安卓和IOS端
·JAVA编写后端逻辑
·PHP编写视频解析逻辑
·VUE编写解析视频网页端
·sh脚本 ffmpeg视频解码转码推流

https://firefix.cn/

应用介绍:最近在研究自媒体,发现抖音上面一些主播24小时不休息一直在播放几部电影,发现我们熬夜在看直播,主播其实已经在睡觉,我们看到的人不一定是真人,方便自己做自媒体 开发了TobeSaver 功能如下

1.无需真人出镜,也不需要电脑和手机 提前录制好的视频素材通过云服务器推流直播

1.Flutter直播推流页面


import 'package:downloaderx/utils/exit.dart';
import 'package:downloaderx/widget/live_type_item.dart';
import 'package:downloaderx/widget/platform_item.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:loading_animation_widget/loading_animation_widget.dart';
import 'package:url_launcher/url_launcher.dart';

import '../network/http_api.dart';
import '../network/http_utils.dart';
import '../utils/event_bus.dart';
import 'login_page.dart';
import 'tutorial_page.dart';

class PushStreamPage extends StatefulWidget {
  const PushStreamPage({super.key});

  @override
  State<PushStreamPage> createState() => _PushStreamPageState();
}

class _PushStreamPageState extends State<PushStreamPage>
    with SingleTickerProviderStateMixin {
  List<dynamic> platform = [
    {'title': '抖音', 'icon': 'douyin.png'},
    {'title': '快手', 'icon': 'ks.png'},
    {'title': '哔哩', 'icon': 'bili.png'},
    {'title': '微博', 'icon': 'weibo.png'},
    {'title': '知乎', 'icon': 'zhihu.png'},
    // {'title': 'YouTobe', 'icon': 'youtobe.png'},
  ];
  List<dynamic> liveType = [
    {'title': '催眠直播', 'icon': 'iconspdy.png', 'type': 0},
    {'title': '音乐直播', 'icon': 'iconspbilibili.png', 'type': 1},
    {'title': '电影直播', 'icon': 'icon_sp_acfun.png', 'type': 2},
  ];
  var currentPlatformIndex = 0;
  var currentLiveIndex = 0;
  bool isCircular = false;
  int status = -1;
  var countdown = 0;
  var controllerHost = TextEditingController(text: "");
  var controllerSecretKey = TextEditingController(text: "");
  var controllerLiveUrl = TextEditingController(text: "");

  @override
  void initState() {
    super.initState();
    loadPushStreamInfo();
    EventBus.getDefault().register(this, (event) {
      if (event.toString() == "refresh_push_stream") {
        loadPushStreamInfo();
      }
    });
  }

  @override
  void dispose() {
    super.dispose();
    EventBus.getDefault().unregister(this);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        title: const Text(
          "直播推流",
        ),
      ),
      body: Container(
        margin: EdgeInsets.symmetric(vertical: 20.w, horizontal: 40.w),
        child: CustomScrollView(
          keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
          slivers: [
            SliverToBoxAdapter(
              child: Container(
                alignment: Alignment.centerLeft,
                margin: EdgeInsets.fromLTRB(0, 0, 0, 25.w),
                child: Text(
                  "选择推流平台",
                  style:
                      TextStyle(fontWeight: FontWeight.bold, fontSize: 30.sp),
                ),
              ),
            ),
            SliverGrid.builder(
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 3,
                  crossAxisSpacing: 10.w,
                  mainAxisSpacing: 10.w,
                  childAspectRatio: 2.2),
              itemCount: platform.length,
              itemBuilder: (BuildContext context, int index) {
                return PlatFormItem(
                  item: platform[index],
                  isSelected: index == currentPlatformIndex,
                  onItemClick: onItemClick,
                );
              },
            ),
            buildInputContainer("服务器地址:", '请输入服务器地址', controllerHost, context),
            buildInputContainer(
                "串流秘钥:", '请输入串流秘钥', controllerSecretKey, context),
            buildInputContainer(
                "直播间地址:", '请输入直播地址', controllerLiveUrl, context),
            SliverToBoxAdapter(
              child: Container(
                alignment: Alignment.centerLeft,
                margin: EdgeInsets.fromLTRB(0, 0, 0, 25.w),
                child: Text(
                  "直播类型",
                  style:
                      TextStyle(fontWeight: FontWeight.bold, fontSize: 30.sp),
                ),
              ),
            ),
            SliverGrid.builder(
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 3,
                  crossAxisSpacing: 10.w,
                  mainAxisSpacing: 10.w,
                  childAspectRatio: 2.6),
              itemCount: liveType.length,
              itemBuilder: (BuildContext context, int index) {
                return LiveTypeItem(
                  item: liveType[index],
                  isSelected: index == currentLiveIndex,
                  onItemClick: onLiveItemClick,
                );
              },
            ),
            SliverToBoxAdapter(
              child: Container(
                margin: EdgeInsets.symmetric(vertical: 50.w, horizontal: 0),
                child: Column(
                  children: [
                    GestureDetector(
                      onTap: onTap,
                      child: AnimatedContainer(
                        duration: Duration(milliseconds: 600),
                        curve: Curves.linear,
                        width: isCircular ? 100.h : 600.w,
                        height: isCircular ? 100.h : 80.h,
                        decoration: BoxDecoration(
                            borderRadius:
                                BorderRadius.circular(isCircular ? 50.h : 40.h),
                            color: Theme.of(context).primaryColor),
                        alignment: Alignment.center,
                        child: isCircular
                            ? countdown > 0
                                ? Stack(
                                    alignment: Alignment.center,
                                    children: [
                                      LoadingAnimationWidget.threeArchedCircle(
                                        color: Colors.white,
                                        size: 60.h,
                                      ),
                                      Text(
                                        "${countdown}s",
                                        style: TextStyle(color: Colors.white),
                                      )
                                    ],
                                  )
                                : LoadingAnimationWidget.hexagonDots(
                                    color: Colors.white,
                                    size: 60.h,
                                  )
                            : Center(
                                child: Text(
                                status == -1
                                    ? '开始推流'
                                    : status == 0
                                        ? "正在排队中..."
                                        : "观看直播",
                                style: TextStyle(
                                    color: Colors.white,
                                    fontSize: 30.sp,
                                    fontWeight: FontWeight.bold),
                              )),
                        // child: isCircular
                        //     ? ClipOval(child: Container(color: Colors.blue))
                        //     : null,
                      ),
                    ),
                  ],
                ),
              ),
            )
          ],
        ),
      ),
    );
  }

  void onTap() async {
    var host = controllerHost.value.text;
    var secretKey = controllerSecretKey.value.text;
    var liveUrl = controllerLiveUrl.value.text;
    var plat = platform[currentPlatformIndex]['title'];
    var type = liveType[currentLiveIndex]['type'];
    if (await UserExit.isLogin() == null) {
      Navigator.push(
          context, MaterialPageRoute(builder: (context) => const LoginPage()));
      return;
    }
    if (host.isEmpty) {
      ToastExit.show('请输入服务器地址');
      return;
    }
    if (secretKey.isEmpty) {
      ToastExit.show('请输入秘钥地址');
      return;
    }
    if (liveUrl.isEmpty) {
      ToastExit.show('请输入直播地址');
      return;
    }
    if (isCircular) {
      return;
    }
    if (status == 0) {
      ToastExit.show("正在排队推流中");
      return;
    } else if (status == 1) {
      jumpLaunchUrl(liveUrl);
      return;
    }

    setState(() {
      countdown = 0;
      isCircular = !isCircular;
    });
    var map = <String, dynamic>{};
    map['liveHost'] = host;
    map['secretKey'] = secretKey;
    map['liveUrl'] = liveUrl;
    map['platform'] = plat;
    map['liveType'] = type;
    var respond = await HttpUtils.instance.requestNetWorkAy(
        Method.post, HttpApi.submitLiveStream,
        queryParameters: map);
    if (respond != null) {
      await Future.delayed(Duration(milliseconds: 400));
      status = 0;
      startTimer();
    }
 
  }

  void startTimer() {
    ToastExit.show("已提交,正在排队等候推流中~");
    countdown = 9;
    Timer.periodic(const Duration(seconds: 1), (Timer timer) {
      if (countdown == 1) {
        timer.cancel();
        setState(() {
          isCircular = false;
          status = 0;
        });
      } else {
        setState(() {
          countdown--;
        });
      }
    });
  }

  void loadPushStreamInfo() async {
    if (await UserExit.isLogin() != null) {
      var respond = await HttpUtils.instance
          .requestNetWorkAy(Method.get, HttpApi.getStreamInfo);
      print(">>>>>>loadPushStreamInfo>>>>>>>>${respond}");
      if (respond != null) {
        setState(() {
          controllerHost.text = respond['liveHost'];
          controllerSecretKey.text = respond['secretKey'];
          controllerLiveUrl.text = respond['liveUrl'];
          status = respond['status'];
          currentPlatformIndex = platform
              .indexWhere((element) => element['title'] == respond['platform']);
          currentLiveIndex = liveType
              .indexWhere((element) => element['type'] == respond['liveType']);
        });
      }
    }
  }

  Future<void> jumpLaunchUrl(webUrl) async {
    final Uri uri = Uri.parse(webUrl);
    if (!await launchUrl(uri)) {
      throw Exception('Could not launch $uri');
    }
  }

  void onItemClick(item) {
    currentPlatformIndex = platform.indexOf(item);
    setState(() {});
  }

  void onLiveItemClick(item) {
    currentLiveIndex = liveType.indexOf(item);
    setState(() {});
  }
}

SliverToBoxAdapter buildInputContainer(String title, String hitText,
    TextEditingController controller, BuildContext context) {
  return SliverToBoxAdapter(
    child: Container(
      width: double.infinity,
      margin: EdgeInsets.fromLTRB(0, 25.w, 0, 0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          GestureDetector(
            onTap: () {
              Navigator.push(
                  context,
                  MaterialPageRoute(
                      builder: (context) => const TutorialPage()));
            },
            child: Row(
              children: [
                Text(
                  title,
                  style:
                      TextStyle(fontWeight: FontWeight.bold, fontSize: 30.sp),
                ),
                const Icon(
                  Icons.help,
                  color: Colors.grey,
                  size: 16,
                ),
              ],
            ),
          ),
          Container(
            height: 80.w,
            width: double.infinity,
            child: TextField(
              maxLines: 1,
              keyboardType: TextInputType.url,
              textAlignVertical: TextAlignVertical.center,
              controller: controller,
              textInputAction: TextInputAction.done,
              decoration: InputDecoration(
                hintText: hitText,
                hintStyle: const TextStyle(color: Colors.grey),
                border: const OutlineInputBorder(borderSide: BorderSide.none),
                focusedBorder:
                    const OutlineInputBorder(borderSide: BorderSide.none),
                enabledBorder: const OutlineInputBorder(
                  borderSide: BorderSide.none,
                ),
                contentPadding: EdgeInsets.symmetric(horizontal: 0.w),
              ),
            ),
          ),
        ],
      ),
    ),
  );
}

2.java后端处理数据逻辑

3.sh脚本处理推流 

e
w='\033[0;33m'
font="\033[0m"

ffmpeg_install(){
# 安装FFMPEG
read -p "你的机器内是否已经安装过FFmpeg4.x?安装FFmpeg才能正常推流,是否现在安装FFmpeg?(yes/no):" Choose
if [ $Choose = "yes" ];then
	yum -y install wget
	wget --no-check-certificate https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-4.0.3-64bit-static.tar.xz
	tar -xJf ffmpeg-4.0.3-64bit-static.tar.xz
	cd ffmpeg-4.0.3-64bit-static
	mv ffmpeg /usr/bin && mv ffprobe /usr/bin && mv qt-faststart /usr/bin && mv ffmpeg-10bit /usr/bin
fi
if [ $Choose = "no" ]
then
    echo -e "${yellow} 你选择不安装FFmpeg,请确定你的机器内已经自行安装过FFmpeg,否则程序无法正常工作! ${font}"
    sleep 2
fi
	}

stream_start(){
# 定义推流地址和推流码
read -p "输入你的推流地址和推流码(rtmp协议):" rtmp

# 判断用户输入的地址是否合法
if [[ $rtmp =~ "rtmp://" ]];then
	echo -e "${green} 推流地址输入正确,程序将进行下一步操作. ${font}"
  	sleep 2
	else  
  	echo -e "${red} 你输入的地址不合法,请重新运行程序并输入! ${font}"
  	exit 1
fi 

# 定义视频存放目录
read -p "输入你的视频存放目录 (格式仅支持mp4,并且要绝对路径,例如/opt/video):" folder

# 判断是否需要添加水印
read -p "是否需要为视频添加水印?水印位置默认在右上方,需要较好CPU支持(yes/no):" watermark
if [ $watermark = "yes" ];then
	read -p "输入你的水印图片存放绝对路径,例如/opt/image/watermark.jpg (格式支持jpg/png/bmp):" image
	echo -e "${yellow} 添加水印完成,程序将开始推流. ${font}"
	# 循环
	while true
	do
		cd $folder
		for video in $(ls *.mp4)
		do
		ffmpeg -re -i "$video" -i "$image" -filter_complex overlay=W-w-5:5 -c:v libx264 -c:a aac -b:a 192k -strict -2 -f flv ${rtmp}
		done
	done
fi
if [ $watermark = "no" ]
then
    echo -e "${yellow} 你选择不添加水印,程序将开始推流. ${font}"
    # 循环
	while true
	do
		cd $folder
		for video in $(ls *.mp4)
		do
		ffmpeg -re -i "$video" -c:v copy -c:a aac -b:a 192k -strict -2 -f flv ${rtmp}
		done
	done
fi
	}

# 停止推流
stream_stop(){
	screen -S stream -X quit
	killall ffmpeg
	}

# 开始菜单设置
echo -e "${yellow} CentOS7 X86_64 FFmpeg无人值守循环推流 For LALA.IM ${font}"
echo -e "${red} 请确定此脚本目前是在screen窗口内运行的! ${font}"
echo -e "${green} 1.安装FFmpeg (机器要安装FFmpeg才能正常推流) ${font}"
echo -e "${green} 2.开始无人值守循环推流 ${font}"
echo -e "${green} 3.停止推流 ${font}"
start_menu(){
    read -p "请输入数字(1-3),选择你要进行的操作:" num
    case "$num" in
        1)
        ffmpeg_install
        ;;
        2)
        stream_start
        ;;
        3)
        stream_stop
        ;;
        *)
        echo -e "${red} 请输入正确的数字 (1-3) ${font}"
        ;;
    esac
	}

# 运行开始菜单
start_menu

4. Vue简单视频解析网站

归纳总结:

此项目话费10个天时间,业余时间编写,搭建在2核4G的服务器上,一开始方便自己在自媒体上创作高质量视频,期间一段时间比较忙就没管了,后面不知什么时候被别人拿去用,有一天服务器发来了一封预警邮件,我一看cpu,内存高达99%,然后看了下数据库累计用户高达一万,网站日活高达300uv

整理一份相关文档供大家参考

创作不易,点关注不迷路!

技术链接彼此、代码拥抱未来!

下载地址icon-default.png?t=N7T8https://firefix.cn/

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
从引用内容中可以得知,使用ffmpeg可以实现直播推流。首先需要下载ffmpeg和ffplay,并将其与成品软件放置在同一个目录下。ffmpeg是一个跨平台的工具,支持多平台多账号同时推流本地视频,实现无人直播推流的地址统一为rtmp://127.0.0.1:1935/live/123。使用ffmpeg推流的方法与本地视频推流类似,只需将摄像头拍到的画面实时推流出去即可。注意,推流的过程可能会有一定的延迟,并且默认情况下没有声音。如果需要推流麦克风的声音,也可以进行相应的设置。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [利用ffmpeg实现rtmp推流直播](https://blog.csdn.net/gqg_guan/article/details/129144426)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [易语言-ffmpeg直播推流,可实现抖音等全平台无人直播](https://download.csdn.net/download/weixin_38686231/19833828)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GodCodeApps

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值