├── looplive
├── __init__.py
├── execute
│ ├── __init__.py
│ └── scan_and_execute.py
├── model
│ ├── __init__.py
│ └── model.py
├── tests
│ ├── __init__.py
│ └── test_execute.py
├── controller
│ ├── __init__.py
│ ├── bili_controller.py
│ ├── ytb_controller.py
│ └── config_controller.py
├── main.py
└── cli.py
├── .dockerignore
├── Dockerfile
├── .github
├── ISSUE_TEMPLATE
│ ├── docs-feedback.md
│ ├── feature_request.md
│ ├── bug_report.md
│ └── help-wanted.md
└── pull_request_template.md
├── pyproject.toml
├── LICENSE
├── assets
├── youtube.svg
└── bilibili.svg
├── README.md
├── README-en.md
└── .gitignore
/looplive/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/looplive/execute/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/looplive/model/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/looplive/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/looplive/controller/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | dist/*
2 | __pycache__/
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8-slim
2 |
3 | LABEL maintainer="timerring"
4 |
5 | WORKDIR /app
6 |
7 | COPY . /app
8 |
9 | RUN apt-get update && apt-get install -y ffmpeg \
10 | && apt-get clean \
11 | && rm -rf /var/lib/apt/lists/*
12 |
13 | ENV TZ="Asia/Shanghai"
14 |
15 | CMD ["python", "-m", "looplive.main"]
--------------------------------------------------------------------------------
/looplive/tests/test_execute.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 looplive
2 |
3 | import unittest
4 | from unittest.mock import patch, MagicMock
5 | from looplive.execute.scan_and_execute import scan_folder_and_execute
6 |
7 | class TestScanAndExecute(unittest.TestCase):
8 |
9 | def test_scan_folder_and_execute(self):
10 | folder_to_scan = "/home/Downloads/looplive/videos"
11 | command_to_execute = "ffmpeg -re -i {file_path} -c copy -f flv {stream_url}"
12 | scan_folder_and_execute(folder_to_scan, command_to_execute)
--------------------------------------------------------------------------------
/looplive/execute/scan_and_execute.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 looplive
2 |
3 | import os
4 | import subprocess
5 |
6 | def scan_folder_and_execute(folder_path, stream_url):
7 | files = sorted(os.listdir(folder_path))
8 | for file in files:
9 | if file.lower().endswith(('.flv', '.mp4')):
10 | file_path = os.path.join(folder_path, file)
11 | if os.path.isfile(file_path):
12 | command = f'ffmpeg -re -i {file_path} -c copy -f flv "{stream_url}"'
13 | subprocess.run(command, shell=True, check=True)
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/docs-feedback.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Docs feedback
3 | about: Improve documentation about this project.
4 | title: "[Docs] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Documentation Reference
11 | (Path/Link to the documentation file)
12 |
13 | ## Feedback on documentation
14 | (Your suggestions to the documentation. e.g., accuracy, complex explanations, structural organization, practical examples, technical reliability, and consistency)
15 |
16 | ## Additional context
17 | (Add any other context or screenshots about the documentation here.)
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project.
4 | title: "[Feature] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Is your feature request related to a problem? Please describe.
11 | (A clear and concise description of what the problem is.)
12 |
13 | ## Describe the solution you'd like
14 | (A clear and concise description of what you want to happen.)
15 |
16 | ## Describe alternatives you've considered
17 | (A clear and concise description of any alternative solutions or features you've considered.)
18 |
19 | ## Additional context
20 | (Add any other context or screenshots about the feature request here.)
21 |
--------------------------------------------------------------------------------
/looplive/controller/bili_controller.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 looplive
2 |
3 | import subprocess
4 | from looplive.execute.scan_and_execute import scan_folder_and_execute
5 | from looplive.controller.config_controller import ConfigController
6 |
7 | class BiliController:
8 | def __init__(self, cc: ConfigController):
9 | self.server_url = cc.get_config()['bili_server_url']
10 | self.key = cc.get_config()['bili_key']
11 | self.folder = cc.get_config()['folder']
12 |
13 | @property
14 | def stream_url(self):
15 | return f'{self.server_url}{self.key}'
16 |
17 | def stream(self):
18 | while True:
19 | scan_folder_and_execute(self.folder, self.stream_url)
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "looplive"
7 | version = "0.0.2"
8 | authors = [
9 | { name="timerring"},
10 | ]
11 | description = "The Python toolkit package and cli designed for auto loop live."
12 | readme = "README.md"
13 | license = { file="LICENSE" }
14 | requires-python = ">=3.6"
15 | classifiers = [
16 | "Programming Language :: Python :: 3",
17 | "License :: OSI Approved :: MIT License",
18 | "Operating System :: OS Independent",
19 | ]
20 | dependencies = []
21 |
22 | [project.scripts]
23 | looplive = "looplive.cli:cli"
24 |
25 | [project.urls]
26 | "Homepage" = "https://github.com/timerring/looplive"
27 |
--------------------------------------------------------------------------------
/looplive/controller/ytb_controller.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 looplive
2 |
3 | import subprocess
4 | from looplive.execute.scan_and_execute import scan_folder_and_execute
5 | from looplive.controller.config_controller import ConfigController
6 |
7 | class YtbController:
8 | def __init__(self, cc: ConfigController):
9 | self.server_url = cc.get_config()['ytb_server_url']
10 | self.key = cc.get_config()['ytb_key']
11 | self.folder = cc.get_config()['folder']
12 |
13 | @property
14 | def stream_url(self):
15 | # Add leading slash to key if it doesn't start with one
16 | key = self.key if self.key.startswith('/') else f'/{self.key}'
17 | return f'{self.server_url}{key}'
18 |
19 | def stream(self):
20 | while True:
21 | scan_folder_and_execute(self.folder, self.stream_url)
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 | ## Description
3 |
4 | [Please describe the background, purpose, changes made, and how to test this PR]
5 |
6 | ## Related Issues
7 |
8 | [List the issue numbers related to this PR]
9 |
10 | ## Changes Proposed
11 |
12 | - [ ] change1
13 | - [ ] ...
14 |
15 | ## Who Can Review?
16 |
17 | [Please use the '@' symbol to mention any community member who is free to review the PR once the tests have passed. Feel free to tag members or contributors who might be interested in your PR.]
18 |
19 | ## TODO
20 |
21 | - [ ] task1
22 | - [ ] ...
23 |
24 | ## Checklist
25 |
26 | - [ ] Code has been reviewed
27 | - [ ] Code complies with the project's code standards and best practices
28 | - [ ] Code has passed all tests
29 | - [ ] Code does not affect the normal use of existing features
30 | - [ ] Code has been commented properly
31 | - [ ] Documentation has been updated (if applicable)
32 | - [ ] Demo/checkpoint has been attached (if applicable)
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve this project.
4 | title: "[BUG] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Describe the bug
11 | (A clear and concise description of what the bug is.)
12 |
13 | ## How To Reproduce
14 | Steps to reproduce the behavior:
15 | 1. Config/File changes: ...
16 | 2. Run command: ...
17 | 3. See error: ...
18 |
19 | ## Expected behavior
20 | (A clear and concise description of what you expected to happen.)
21 |
22 | ## Screenshots
23 | (If applicable, add screenshots to help explain your problem.)
24 |
25 | ## Environment Information
26 | - Operating System: [e.g. Ubuntu 20.04.5 LTS]
27 | - Python Version: [e.g. Python 3.9.15]
28 | - Driver & CUDA Version: [e.g. Driver 470.103.01 & CUDA 11.4]
29 | - Error Messages and Logs: [If applicable, provide any error messages or relevant log outputs]
30 |
31 | ## Additional context
32 | (Add any other context about the problem here.)
33 |
--------------------------------------------------------------------------------
/looplive/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | import threading
3 | from looplive.model.model import Model
4 | from looplive.controller.config_controller import ConfigController
5 | from looplive.controller.bili_controller import BiliController
6 | from looplive.controller.ytb_controller import YtbController
7 | if __name__ == '__main__':
8 | path = os.path.join(os.path.dirname(__file__), 'model/config.json')
9 | cc = ConfigController(path=path)
10 | if cc.check_bili_config() and cc.check_ytb_config():
11 | bili_thread = threading.Thread(target=lambda: BiliController(cc).stream())
12 | ytb_thread = threading.Thread(target=lambda: YtbController(cc).stream())
13 |
14 | bili_thread.start()
15 | ytb_thread.start()
16 |
17 | bili_thread.join()
18 | ytb_thread.join()
19 | elif cc.check_bili_config():
20 | BiliController(cc).stream()
21 | elif cc.check_ytb_config():
22 | YtbController(cc).stream()
23 | else:
24 | print("Please complete the configuration first!")
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/help-wanted.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Help wanted
3 | about: Want help from this project team.
4 | title: "[Help] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Problem Overview
11 | (Briefly and clearly describe the issue you're facing and seeking help with.)
12 |
13 | ## Steps Taken
14 | (Detail your attempts to resolve the issue, including any relevant steps or processes.)
15 | 1. Config/File changes: ...
16 | 2. Run command: ...
17 | 3. See errors: ...
18 |
19 | ## Expected Outcome
20 | (A clear and concise description of what you expected to happen.)
21 |
22 | ## Screenshots
23 | (If applicable, add screenshots to help explain your problem.)
24 |
25 | ## Environment Information
26 | - Operating System: [e.g. Ubuntu 20.04.5 LTS]
27 | - Python Version: [e.g. Python 3.9.15]
28 | - Driver & CUDA Version: [e.g. Driver 470.103.01 & CUDA 11.4]
29 | - Error Messages and Logs: [If applicable, provide any error messages or relevant log outputs]
30 |
31 | ## Additional context
32 | (Add any other context about the problem here.)
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 John Howe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/looplive/model/model.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 looplive
2 |
3 | import json
4 | import os
5 |
6 | class Model:
7 | def __init__(self, path=None) -> None:
8 | if path is None:
9 | self.path = os.path.join(os.path.dirname(__file__), "config.json")
10 | else:
11 | self.path = path
12 | self.default_config = {
13 | "folder": "",
14 | "bili_server_url": "",
15 | "bili_key": "",
16 | "ytb_server_url": "",
17 | "ytb_key": ""
18 | }
19 |
20 | def get_default_config(self):
21 | return self.default_config
22 |
23 | def reset_config(self):
24 | self.write(self.default_config)
25 |
26 | def update_specific_config(self, key, value):
27 | config_info = self.get_config()
28 | config_info[key] = value
29 | self.write(config_info)
30 |
31 | def update_multiple_config(self, updates: dict):
32 | config_info = self.get_config()
33 | for key, value in updates.items():
34 | config_info[key] = value
35 | self.write(config_info)
36 |
37 | def get_config(self):
38 | if not os.path.exists(self.path):
39 | self.reset_config()
40 | return self.read()
41 |
42 | def read(self):
43 | with open(self.path, "r") as f:
44 | return json.load(f)
45 |
46 | def write(self, config):
47 | with open(self.path, "w") as f:
48 | json.dump(config, f, indent=4)
49 |
--------------------------------------------------------------------------------
/looplive/controller/config_controller.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 looplive
2 |
3 | from looplive.model.model import Model
4 |
5 | class ConfigController:
6 | def __init__(self, path=None):
7 | self.model = Model(path=path)
8 |
9 | def check_folder_config(self):
10 | config_info = self.model.get_config()
11 | if not config_info['folder']:
12 | print("The folder is not complete!", flush=True)
13 | return False
14 | return True
15 |
16 | def check_bili_config(self):
17 | config_info = self.model.get_config()
18 | if not self.check_folder_config() or not config_info['bili_server_url'] or not config_info['bili_key']:
19 | print("The bilibili configuration is not complete!", flush=True)
20 | return False
21 | return True
22 |
23 | def check_ytb_config(self):
24 | config_info = self.model.get_config()
25 | if not self.check_folder_config() or not config_info['ytb_server_url'] or not config_info['ytb_key']:
26 | print("The youtube configuration is not complete!", flush=True)
27 | return False
28 | return True
29 |
30 | def update_config(self, config_info):
31 | self.model.update_multiple_config(config_info)
32 |
33 | def reset_config(self):
34 | self.model.reset_config()
35 |
36 | def get_config(self):
37 | return self.model.get_config()
38 |
39 | def get_specific_config(self, key):
40 | return self.model.get_specific_config(key)
41 |
42 | def update_specific_config(self, key, value):
43 | self.model.update_specific_config(key, value)
44 |
--------------------------------------------------------------------------------
/assets/youtube.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/bilibili.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 简体中文 | [English](./README-en.md)
9 |
10 | `looplive` 是一个 7 x 24 小时全自动**循环多平台同时推流**直播工具。
11 |
12 | 支持平台(支持更多平台欢迎[issue](https://github.com/timerring/looplive/issues))
13 |
14 |

15 |

16 |
17 |
18 |
19 | ## 特点
20 |
21 | > 欢迎使用,欢迎提供更多反馈,欢迎 PR 贡献此项目,请勿用于违反社区规定的用途。
22 |
23 | - 支持 7 x 24 小时**全自动循环直播**推流
24 | - 支持多平台**同时直播**:
25 | - Bilibili
26 | - Youtube
27 | - 理论上只要平台有 rtmp 服务器就能支持,增加新的平台欢迎 [issue](https://github.com/timerring/looplive/issues)。
28 | - 支持记忆参数,**仅需添加一次**,后续一键自动运行
29 | - 支持 Docker 部署
30 |
31 | ## 提前准备
32 |
33 | 使用此工具前,您需要先安装ffmpeg:
34 |
35 | - Windows: `choco install ffmpeg`(通过[Chocolatey](https://chocolatey.org/))或其他方法
36 | - macOS: `brew install ffmpeg`(通过[Homebrew](https://brew.sh/))
37 | - Linux: `sudo apt install ffmpeg`(Debian/Ubuntu)
38 |
39 | 更多操作系统安装 ffmpeg 请参考[官方网站](https://ffmpeg.org/download.html)。
40 |
41 | 然后安装 looplive
42 |
43 | ```bash
44 | pip install looplive
45 | ```
46 |
47 | ## 快速开始
48 |
49 | ### 开始直播
50 |
51 | #### Bilibili
52 |
53 | 1. 前往 [直播页面](https://link.bilibili.com/p/center/index#/my-room/start-live).
54 | - 如果你还没有直播权限,请先申请,点击 `立即开通直播间`,然后按照 b 站提示操作。
55 | 2. 点击 `开始直播`.
56 | 3. 获取推流服务器地址 `-bs` 和串流密钥 `-bk`,如下图所示,直接复制即可。
57 |
58 | > 为了避免命令参数被错误分隔,请使用英文双引号 `"` 包裹每一项参数。
59 |
60 | 
61 |
62 | #### Youtube
63 |
64 | 1. 前往 [直播页面](https://www.youtube.com/live_dashboard).
65 | 2. 获取推流服务器地址(Stream URL) `-ys` 和串流密钥(Stream key) `-yk`。
66 |
67 | > 为了避免命令参数被错误分隔,请使用英文双引号 `"` 包裹每一项参数。
68 |
69 | 
70 |
71 | #### 视频文件夹
72 |
73 | **参数 `-f` 是视频文件的存放文件夹**
74 |
75 | > 为了避免命令参数被错误分隔,请使用英文双引号 `"` 包裹每一项参数。可以只填需要的平台对应的 server_url 和 key。
76 |
77 | ```bash
78 | # eg.
79 | # 只需要推流到 Bilibili 的配置,只需添加一次,例如
80 | looplive add -bs "rtmp://live-push.bilivideo.com/live-bvc/" -bk "?streamname=live_3541234541234567_8901234&key=looplivexxxxxxxxxxxxdgd&schedule=rtmp&pflag=1" -f "your/folder/path"
81 | # 只需要推流到 Youtube 的配置,只需添加一次
82 | looplive add -ys "rtmp://a.rtmp.youtube.com/live2" -yk "ghkh-sfgg-loop-live-live" -f "your/folder/path"
83 | # 同时推流到 Bilibili 和 Youtube 的配置,只需添加一次
84 | looplive add -bs "rtmp://live-push.bilivideo.com/live-bvc/" -bk "?streamname=live_3541234541234567_8901234&key=looplivexxxxxxxxxxxxdgd&schedule=rtmp&pflag=1" -ys "rtmp://a.rtmp.youtube.com/live2" -yk "ghkh-sfgg-loop-live-live" -f "your/folder/path"
85 | ```
86 |
87 | ### 推流
88 |
89 | 只需添加一次参数,以后直接执行以下命令启动即可。
90 |
91 | ```bash
92 | # 只推流到 Bilibili
93 | looplive bili
94 | # 只推流到 Youtube
95 | looplive youtube
96 | # 同时推流到 Bilibili 和 Youtube
97 | looplive both
98 | ```
99 |
100 | ## Docker 部署
101 |
102 | ### 配置文件
103 |
104 | ```json
105 | {
106 | "folder": "/app/looplive/videos", // 由于 docker 的挂载映射,最好不要修改这里
107 | "bili_server_url": "rtmp://live-push.bilivideo.com/live-bvc/",
108 | "bili_key": "?streamname=live_3541234541234567_8901234&key=looplivexxxxxxxxxxxxdgd&schedule=rtmp&pflag=1",
109 | "ytb_server_url": "rtmp://a.rtmp.youtube.com/live2", // 不需要可置为空 ""
110 | "ytb_key": "ghkh-sfgg-loop-live-live" // 不需要可置为空 ""
111 | }
112 | ```
113 |
114 | ### 运行
115 |
116 | ```bash
117 | sudo docker run -it \
118 | -v /your/path/to/config.json:/app/looplive/model/config.json \
119 | -v /your/path/to/videos:/app/looplive/videos \
120 | --name looplive_docker \
121 | ghcr.io/timerring/looplive:0.0.2
122 | ```
123 |
124 | ### 更多用法
125 |
126 | ```bash
127 | $ looplive -h
128 |
129 | looplive [-h] [-V] {check,add,reset,bili} ...
130 |
131 |
132 | The Python toolkit package and cli designed for auto loop live.
133 |
134 | positional arguments:
135 | {check,add,reset,bili,ytb,both}
136 | Subcommands
137 | check Check the configuration
138 | add Add the configuration
139 | reset Reset the configuration
140 | bili Stream on the bilibili platform
141 | ytb Stream on the youtube platform
142 | both Stream on the bilibili and youtube platform
143 |
144 | options:
145 | -h, --help show this help message and exit
146 | -V, --version Print version information
147 | ```
--------------------------------------------------------------------------------
/looplive/cli.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025 looplive
2 |
3 | import argparse
4 | import sys
5 | import os
6 | import logging
7 | import threading
8 | import textwrap
9 | from looplive.controller.bili_controller import BiliController
10 | from looplive.controller.ytb_controller import YtbController
11 | from looplive.controller.config_controller import ConfigController
12 |
13 |
14 | def cli():
15 | parser = argparse.ArgumentParser(
16 | prog="looplive",
17 | formatter_class=argparse.RawDescriptionHelpFormatter,
18 | description=textwrap.dedent('''
19 | The Python toolkit package and cli designed for auto loop live.
20 | Source code at https://github.com/timerring/looplive
21 | '''),
22 | epilog=textwrap.dedent('''
23 | Example:
24 | looplive check
25 | looplive add -bs "rtmp://xxx" -bk "?streamname=xxx" -ys "rtmp://xxx" -yk "xxx-xxx-xxx-xxx" -f "your/folder/path"
26 | looplive bili
27 | looplive ytb
28 | looplive both
29 | '''),
30 | )
31 | parser.add_argument(
32 | "-V",
33 | "--version",
34 | action="version",
35 | version="looplive 0.0.2 and source code at https://github.com/timerring/looplive",
36 | help="Print version information",
37 | )
38 |
39 | subparsers = parser.add_subparsers(dest='subcommand', help='Subcommands')
40 |
41 | # Check config
42 | check_config_parser = subparsers.add_parser('check', help='Check the configuration')
43 |
44 | # Add config
45 | add_config_parser = subparsers.add_parser('add', help='Add the configuration')
46 | add_config_parser.add_argument('-bs', '--bili_server_url', help='The bilibili server url')
47 | add_config_parser.add_argument('-bk', '--bili_key', help='The bilibili stream key')
48 | add_config_parser.add_argument('-ys', '--ytb_server_url', help='The youtube server url')
49 | add_config_parser.add_argument('-yk', '--ytb_key', help='The youtube stream key')
50 | add_config_parser.add_argument('-f', '--folder', help='The input folder')
51 |
52 | # Reset config
53 | reset_config_parser = subparsers.add_parser('reset', help='Reset the configuration')
54 |
55 | # Bilibili stream
56 | bilibili_parser = subparsers.add_parser('bili', help='Stream on the bilibili platform')
57 |
58 | # Youtube stream
59 | youtube_parser = subparsers.add_parser('ytb', help='Stream on the youtube platform')
60 |
61 | # Both stream
62 | both_parser = subparsers.add_parser('both', help='Stream on the bilibili and youtube platform')
63 |
64 | args = parser.parse_args()
65 |
66 | # Check if no subcommand is provided
67 | if args.subcommand is None:
68 | print("No subcommand provided. Please specify a subcommand.")
69 | parser.print_help()
70 | sys.exit()
71 |
72 | if args.subcommand == 'check':
73 | print(ConfigController().get_config())
74 |
75 | if args.subcommand == 'add':
76 | if args.bili_server_url:
77 | ConfigController().update_specific_config('bili_server_url', args.bili_server_url)
78 | if args.bili_key:
79 | ConfigController().update_specific_config('bili_key', args.bili_key)
80 | if args.ytb_server_url:
81 | ConfigController().update_specific_config('ytb_server_url', args.ytb_server_url)
82 | if args.ytb_key:
83 | ConfigController().update_specific_config('ytb_key', args.ytb_key)
84 | if args.folder:
85 | ConfigController().update_specific_config('folder', args.folder)
86 |
87 | if args.subcommand == 'reset':
88 | ConfigController().reset_config()
89 |
90 | if args.subcommand == 'bili':
91 | cc = ConfigController()
92 | if cc.check_bili_config():
93 | BiliController(cc).stream()
94 | else:
95 | print("Please complete the bilibili configuration first!", flush=True)
96 |
97 | if args.subcommand == 'ytb':
98 | cc = ConfigController()
99 | if cc.check_ytb_config():
100 | YtbController(cc).stream()
101 | else:
102 | print("Please complete the youtube configuration first!", flush=True)
103 |
104 | if args.subcommand == 'both':
105 | cc = ConfigController()
106 | if cc.check_bili_config() and cc.check_ytb_config():
107 | # two threads
108 | bili_thread = threading.Thread(target=lambda: BiliController(cc).stream())
109 | ytb_thread = threading.Thread(target=lambda: YtbController(cc).stream())
110 |
111 | bili_thread.start()
112 | ytb_thread.start()
113 |
114 | bili_thread.join()
115 | ytb_thread.join()
116 | else:
117 | print("Please complete the configuration first!", flush=True)
118 |
119 | if __name__ == '__main__':
120 | cli()
--------------------------------------------------------------------------------
/README-en.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | [简体中文](./README.md) | English
9 |
10 | `looplive` is a 24/7 automated **multi-platform simultaneous streaming** tool.
11 |
12 | Supported Platforms (support more platforms welcome via [issue](https://github.com/timerring/looplive/issues))
13 |
14 |

15 |

16 |
17 |
18 |
19 | ## Features
20 |
21 | > Welcome to use, feedback is appreciated, PRs are welcome. Please do not use for purposes that violate community guidelines.
22 |
23 | - Supports 24/7 **automated loop streaming**
24 | - Supports **simultaneous streaming** to multiple platforms:
25 | - Bilibili
26 | - Youtube
27 | - Theoretically supports any platform with RTMP servers, more platforms welcome via [issue](https://github.com/timerring/looplive/issues)
28 | - Remembers parameters, **configure once**, run automatically afterwards
29 | - Supports Docker deployment
30 |
31 | ## Prerequisites
32 |
33 | Before using this tool, you need to install ffmpeg:
34 |
35 | - Windows: `choco install ffmpeg` (via [Chocolatey](https://chocolatey.org/)) or other methods
36 | - macOS: `brew install ffmpeg` (via [Homebrew](https://brew.sh/))
37 | - Linux: `sudo apt install ffmpeg` (Debian/Ubuntu)
38 |
39 | For other operating systems, please refer to the [official website](https://ffmpeg.org/download.html).
40 |
41 | Then install looplive:
42 |
43 | ```bash
44 | pip install looplive
45 | ```
46 |
47 | ## Quick Start
48 |
49 | ### Start Streaming
50 |
51 | #### Bilibili
52 |
53 | 1. Go to the [live streaming page](https://link.bilibili.com/p/center/index#/my-room/start-live).
54 | - If you don't have streaming permissions yet, apply first by clicking `Start Live Room` and follow Bilibili's instructions.
55 | 2. Click `Start Live Streaming`.
56 | 3. Get the streaming server URL `-bs` and stream key `-bk` as shown in the image below, just copy them directly.
57 |
58 | > To avoid command parameter parsing issues, please wrap each parameter in English double quotes `"`.
59 |
60 | 
61 |
62 | #### Youtube
63 |
64 | 1. Go to the [live streaming page](https://www.youtube.com/live_dashboard).
65 | 2. Get the Stream URL `-ys` and Stream key `-yk`.
66 |
67 | > To avoid command parameter parsing issues, please wrap each parameter in English double quotes `"`.
68 |
69 | 
70 |
71 | #### Video Folder
72 |
73 | **Parameter `-f` is the folder where video files are stored**
74 |
75 | > To avoid command parameter parsing issues, please wrap each parameter in English double quotes `"`. You only need to fill in the server_url and key for the platforms you want to use.
76 |
77 | ```bash
78 | # eg.
79 | # Configuration for Bilibili only, add once
80 | looplive add -bs "rtmp://live-push.bilivideo.com/live-bvc/" -bk "?streamname=live_3541234541234567_8901234&key=looplivexxxxxxxxxxxxdgd&schedule=rtmp&pflag=1" -f "your/folder/path"
81 | # Configuration for Youtube only, add once
82 | looplive add -ys "rtmp://a.rtmp.youtube.com/live2" -yk "ghkh-sfgg-loop-live-live" -f "your/folder/path"
83 | # Configuration for both Bilibili and Youtube, add once
84 | looplive add -bs "rtmp://live-push.bilivideo.com/live-bvc/" -bk "?streamname=live_3541234541234567_8901234&key=looplivexxxxxxxxxxxxdgd&schedule=rtmp&pflag=1" -ys "rtmp://a.rtmp.youtube.com/live2" -yk "ghkh-sfgg-loop-live-live" -f "your/folder/path"
85 | ```
86 |
87 | ### Streaming
88 |
89 | After adding parameters once, you can start streaming with the following commands.
90 |
91 | ```bash
92 | # Stream to Bilibili only
93 | looplive bili
94 | # Stream to Youtube only
95 | looplive youtube
96 | # Stream to both Bilibili and Youtube
97 | looplive both
98 | ```
99 |
100 | ## Docker Deployment
101 |
102 | ### Configuration File
103 |
104 |
105 | ```json
106 | {
107 | "folder": "/app/looplive/videos", // don't change this
108 | "bili_server_url": "rtmp://live-push.bilivideo.com/live-bvc/",
109 | "bili_key": "?streamname=live_3541234541234567_8901234&key=looplivexxxxxxxxxxxxdgd&schedule=rtmp&pflag=1",
110 | "ytb_server_url": "rtmp://a.rtmp.youtube.com/live2", // Set to "" if not needed
111 | "ytb_key": "ghkh-sfgg-loop-live-live" // Set to "" if not needed
112 | }
113 | ```
114 |
115 | ### Running
116 |
117 | ```bash
118 | sudo docker run -it \
119 | -v /your/path/to/config.json:/app/looplive/model/config.json \
120 | -v /your/path/to/videos:/app/looplive/videos \
121 | --name looplive_docker \
122 | ghcr.io/timerring/looplive:0.0.2
123 | ```
124 |
125 | ### More Usage
126 |
127 | ```bash
128 | $ looplive -h
129 |
130 | looplive [-h] [-V] {check,add,reset,bili} ...
131 |
132 |
133 | The Python toolkit package and cli designed for auto loop live.
134 |
135 | positional arguments:
136 | {check,add,reset,bili,ytb,both}
137 | Subcommands
138 | check Check the configuration
139 | add Add the configuration
140 | reset Reset the configuration
141 | bili Stream on the bilibili platform
142 | ytb Stream on the youtube platform
143 | both Stream on the bilibili and youtube platform
144 |
145 | options:
146 | -h, --help show this help message and exit
147 | -V, --version Print version information
148 | ```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/intellij+all,python,pycharm+all,macos,windows
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,python,pycharm+all,macos,windows
3 |
4 | ### Intellij+all ###
5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
7 |
8 | # User-specific stuff
9 | .idea/**/workspace.xml
10 | .idea/**/tasks.xml
11 | .idea/**/usage.statistics.xml
12 | .idea/**/dictionaries
13 | .idea/**/shelf
14 |
15 | # Generated files
16 | .idea/**/contentModel.xml
17 |
18 | # Sensitive or high-churn files
19 | .idea/**/dataSources/
20 | .idea/**/dataSources.ids
21 | .idea/**/dataSources.local.xml
22 | .idea/**/sqlDataSources.xml
23 | .idea/**/dynamic.xml
24 | .idea/**/uiDesigner.xml
25 | .idea/**/dbnavigator.xml
26 |
27 | # Gradle
28 | .idea/**/gradle.xml
29 | .idea/**/libraries
30 |
31 | # Gradle and Maven with auto-import
32 | # When using Gradle or Maven with auto-import, you should exclude module files,
33 | # since they will be recreated, and may cause churn. Uncomment if using
34 | # auto-import.
35 | # .idea/artifacts
36 | # .idea/compiler.xml
37 | # .idea/jarRepositories.xml
38 | # .idea/modules.xml
39 | # .idea/*.iml
40 | # .idea/modules
41 | # *.iml
42 | # *.ipr
43 |
44 | # CMake
45 | cmake-build-*/
46 |
47 | # Mongo Explorer plugin
48 | .idea/**/mongoSettings.xml
49 |
50 | # File-based project format
51 | *.iws
52 |
53 | # IntelliJ
54 | out/
55 |
56 | # mpeltonen/sbt-idea plugin
57 | .idea_modules/
58 |
59 | # JIRA plugin
60 | atlassian-ide-plugin.xml
61 |
62 | # Cursive Clojure plugin
63 | .idea/replstate.xml
64 |
65 | # Crashlytics plugin (for Android Studio and IntelliJ)
66 | com_crashlytics_export_strings.xml
67 | crashlytics.properties
68 | crashlytics-build.properties
69 | fabric.properties
70 |
71 | # Editor-based Rest Client
72 | .idea/httpRequests
73 |
74 | # Android studio 3.1+ serialized cache file
75 | .idea/caches/build_file_checksums.ser
76 |
77 | ### Intellij+all Patch ###
78 | # Ignores the whole .idea folder and all .iml files
79 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
80 |
81 | .idea/
82 |
83 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
84 |
85 | *.iml
86 | modules.xml
87 | .idea/misc.xml
88 | *.ipr
89 |
90 | # Sonarlint plugin
91 | .idea/sonarlint
92 |
93 | ### macOS ###
94 | # General
95 | .DS_Store
96 | .AppleDouble
97 | .LSOverride
98 |
99 | # Icon must end with two \r
100 | Icon
101 |
102 |
103 | # Thumbnails
104 | ._*
105 |
106 | # Files that might appear in the root of a volume
107 | .DocumentRevisions-V100
108 | .fseventsd
109 | .Spotlight-V100
110 | .TemporaryItems
111 | .Trashes
112 | .VolumeIcon.icns
113 | .com.apple.timemachine.donotpresent
114 |
115 | # Directories potentially created on remote AFP share
116 | .AppleDB
117 | .AppleDesktop
118 | Network Trash Folder
119 | Temporary Items
120 | .apdisk
121 |
122 | ### PyCharm+all ###
123 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
124 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
125 |
126 | # User-specific stuff
127 |
128 | # Generated files
129 |
130 | # Sensitive or high-churn files
131 |
132 | # Gradle
133 |
134 | # Gradle and Maven with auto-import
135 | # When using Gradle or Maven with auto-import, you should exclude module files,
136 | # since they will be recreated, and may cause churn. Uncomment if using
137 | # auto-import.
138 | # .idea/artifacts
139 | # .idea/compiler.xml
140 | # .idea/jarRepositories.xml
141 | # .idea/modules.xml
142 | # .idea/*.iml
143 | # .idea/modules
144 | # *.iml
145 | # *.ipr
146 |
147 | # CMake
148 |
149 | # Mongo Explorer plugin
150 |
151 | # File-based project format
152 |
153 | # IntelliJ
154 |
155 | # mpeltonen/sbt-idea plugin
156 |
157 | # JIRA plugin
158 |
159 | # Cursive Clojure plugin
160 |
161 | # Crashlytics plugin (for Android Studio and IntelliJ)
162 |
163 | # Editor-based Rest Client
164 |
165 | # Android studio 3.1+ serialized cache file
166 |
167 | ### PyCharm+all Patch ###
168 | # Ignores the whole .idea folder and all .iml files
169 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
170 |
171 |
172 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
173 |
174 |
175 | # Sonarlint plugin
176 |
177 | ### Python ###
178 | # Byte-compiled / optimized / DLL files
179 | __pycache__/
180 | *.py[cod]
181 | *$py.class
182 |
183 | # C extensions
184 | *.so
185 |
186 | # Distribution / packaging
187 | .Python
188 | build/
189 | develop-eggs/
190 | dist/
191 | downloads/
192 | eggs/
193 | .eggs/
194 | parts/
195 | sdist/
196 | var/
197 | wheels/
198 | pip-wheel-metadata/
199 | share/python-wheels/
200 | *.egg-info/
201 | .installed.cfg
202 | *.egg
203 | MANIFEST
204 |
205 | # PyInstaller
206 | # Usually these files are written by a python script from a template
207 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
208 | *.manifest
209 | *.spec
210 |
211 | # Installer logs
212 | pip-log.txt
213 | pip-delete-this-directory.txt
214 |
215 | # Unit test / coverage reports
216 | htmlcov/
217 | .tox/
218 | .nox/
219 | .coverage
220 | .coverage.*
221 | .cache
222 | nosetests.xml
223 | coverage.xml
224 | *.cover
225 | *.py,cover
226 | .hypothesis/
227 | .pytest_cache/
228 | pytestdebug.log
229 |
230 | # Translations
231 | *.mo
232 | *.pot
233 |
234 | # Django stuff:
235 | *.log
236 | local_settings.py
237 | db.sqlite3
238 | db.sqlite3-journal
239 |
240 | # Flask stuff:
241 | instance/
242 | .webassets-cache
243 |
244 | # Scrapy stuff:
245 | .scrapy
246 |
247 | # Sphinx documentation
248 | docs/_build/
249 | doc/_build/
250 |
251 | # PyBuilder
252 | target/
253 |
254 | # Jupyter Notebook
255 | .ipynb_checkpoints
256 |
257 | # IPython
258 | profile_default/
259 | ipython_config.py
260 |
261 | # pyenv
262 | .python-version
263 |
264 | # pipenv
265 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
266 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
267 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
268 | # install all needed dependencies.
269 | #Pipfile.lock
270 |
271 | # poetry
272 | #poetry.lock
273 |
274 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
275 | __pypackages__/
276 |
277 | # Celery stuff
278 | celerybeat-schedule
279 | celerybeat.pid
280 |
281 | # SageMath parsed files
282 | *.sage.py
283 |
284 | # Environments
285 | # .env
286 | .env/
287 | .venv/
288 | env/
289 | venv/
290 | ENV/
291 | env.bak/
292 | venv.bak/
293 | pythonenv*
294 |
295 | # Spyder project settings
296 | .spyderproject
297 | .spyproject
298 |
299 | # Rope project settings
300 | .ropeproject
301 |
302 | # mkdocs documentation
303 | /site
304 |
305 | # mypy
306 | .mypy_cache/
307 | .dmypy.json
308 | dmypy.json
309 |
310 | # Pyre type checker
311 | .pyre/
312 |
313 | # pytype static type analyzer
314 | .pytype/
315 |
316 | # operating system-related files
317 | *.DS_Store #file properties cache/storage on macOS
318 | Thumbs.db #thumbnail cache on Windows
319 |
320 | # profiling data
321 | .prof
322 |
323 |
324 | ### Windows ###
325 | # Windows thumbnail cache files
326 | Thumbs.db
327 | Thumbs.db:encryptable
328 | ehthumbs.db
329 | ehthumbs_vista.db
330 |
331 | # Dump file
332 | *.stackdump
333 |
334 | # Folder config file
335 | [Dd]esktop.ini
336 |
337 | # Recycle Bin used on file shares
338 | $RECYCLE.BIN/
339 |
340 | # Windows Installer files
341 | *.cab
342 | *.msi
343 | *.msix
344 | *.msm
345 | *.msp
346 |
347 | # Windows shortcuts
348 | *.lnk
349 |
350 | # End of https://www.toptal.com/developers/gitignore/api/intellij+all,python,pycharm+all,macos,windows
351 |
352 | # Specific for the MacOS
353 | .DS_Store
354 |
355 | # Specific for the looplive
356 | videos/*
--------------------------------------------------------------------------------