乱码三千 – 分享实用IT技术

乱码三千 – 码出一个新世界


  • 首页

  • 归档

  • 搜索

2024年关于新浪图床失效的解决方法

发表于 2024-04-17

前言

很多早期的站长基本使用新浪的图床, 新浪图床免费又稳定, 受到很多人的青睐, 我本人不怎么用新浪图床, 但是手上有一批素材, 引用的是新浪服务器中的图片, 目前这些图片无论是在浏览器还是在Markdown编辑器中都无法显示

image-20240417181012252

image-20240417181029759

这已经不是简单的防盗链了 应该是新浪服务器做了某种屏蔽处理

检测服务器和链接是否正常

首先我们要确保新浪服务器中有该图片文件, 可以通过以下几种方式检测:

  1. 用VScode编辑器中打开文档 然后鼠标停留在图片链接上, 如果能显示图片, 说明该链接有效

    2024-04-17 18-02-18.2024-04-17 18_02_58

  2. 用PostMan或者ApiPost进行请求访问
    image-20240417181652619

  3. 使用下载工具进行下载

    image-20240417181820927

值得庆幸的是 新浪服务器还没有挂 我们还有足够的时间来对图片进行转移和备份

防盗链临时解决方法

为了使图片迅速恢复访问, 我们可以使用第三方缓存服务来解决防盗链问题, 有以下四种方式可行:

  1. WordPress

    1
    https://i0.wp.com/图片地址(图片地址要掉 https://)
  2. Weserv.nl

    1
    https://images.weserv.nl/?url=图片地址
  3. 百度 1:

    1
    https://image.baidu.com/search/down?url=图片地址
  4. 百度 2

    1
    https://gimg2.baidu.com/image_search/&app=2020&src=图片地址(图片地址要去掉 https://)

使用百度的速度相对快一些

Wordpress批量替换

如果你的wordpress博客大量的图片都在新浪中, 那么可以通过以下两种方式进行批量替换:

  1. 静态替换 修改数据库

    修改数据库之前记得先备份数据库 以防止误操作无法还原

    sql命令如下:

    1
    UPDATE wp_posts SET post_content = REPLACE( post_content,  'https://tvax1.sinaimg.cn/',  'https://image.baidu.com/search/down?url=https://tvax1.sinaimg.cn/' )
  2. 动态替换 修改主题代码

    进入wordpress主题编辑器:

    image-20240417185327043

    将以下代码添加到主题页脚foot.php中:

    1
    2
    3
    4
    5
    6
    7
    <script>
    let context = Array.prototype.map.call(document.images, (event) => {
    event.src = event.src.replace("tvax1.sinaimg.cn/", "image.baidu.com/search/down?url=https://tvax1.sinaimg.cn/")

    });

    </script>

    image-20240417185632599

推荐使用动态的方式自动批量替换, 能将降低数据库操作风险, 又方便后期再次更改, 动态的方式唯一不足之处就是图片加载有延迟

如果你追求加载速度和用户体验, 那么建议采用静态替换的方式

图片批量备份

上面这种临时替换方式始终不是长久之计, 我们需要尽快对图片进行备份和迁移 防止新浪图床哪天不再提供服务了

我个人的做法是通过python批量将图片下载到本地 然后进行图床更换, 核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# -*- coding:utf-8 -*-

import os
import sys
from pyquery import PyQuery as pq
import urllib
# 当前文件路径
current_path = os.path.abspath(__file__)
# 父目录
father_path = os.path.abspath(os.path.dirname(current_path) + os.path.sep + ".")

#----- 用于图片下载------
opener=urllib.request.build_opener()
opener.addheaders=[('User-Agent','Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1941.0 Safari/537.36'),('Accept' , 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'),(
'Accept-Encoding' , 'gzip,deflate,sdch'),
('Accept-Language' , 'zh-CN,zh;q=0.8')]
req=urllib.request
req.install_opener(opener)
#----- 用于图片下载------

# 图片输出目录
pic_dir = 'img'
if not os.path.exists(pic_dir):
os.makedirs(pic_dir)


#将网络图片下载到本地
def download_img(url,dir,image_name):
try:
req.urlretrieve(url, dir+image_name)
except Exception as e:
raise e


#采用原图片地址的名称
def get_pic_name_by_url(url):
# 我只需要末尾xxx部分 使用正则替换
image_name = url.split('/')[-1]
return image_name

def img2url(url):
fir="https://cdn.jsdelivr.net/gh/xxx/xxx/img/"

if "sinaimg.cn" in url:
url="https://image.baidu.com/search/down?url="+url
new_name=get_pic_name_by_url(url)
try:
download_img(url,"{parent}/img/".format(parent=father_path),new_name)
except Exception as e:
print(f"图片下载异常: {e}")
return url
return fir+new_name

# 遍历每篇文章
for idx, post in enumerate(posts):
content, lastmodifytime = post

d=pq(content)
for item in d("img").items():
old_url=item.attr("src")
# 将图片下载到本地
new_url=img2url(old_url)
print(new_url)
#替换图片地址
item.attr("src",new_url)

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

内容多平台分发工具汇总

发表于 2024-04-16

前言

文章撰写一次 到处分发 无需繁琐的复制粘贴 节省大量的重复机械劳动力 追求极致的自动化 一键分发工具 无疑是解决了很多创作者的痛点

接下来给大家汇总几个可一键分发的网站

一键分发平台

  • mdnice

    主要是图文的分发

    image-20240418194825613

  • openwrite

    主要是图文的分发

    image-20240417225346730

  • 蚁小二

    不仅可以分发图文 还可以分发视频

    免费版可以同时管理五个自媒体平台账号

    image-20240501102215475

    image-20240501104155880

  • 小火花

    不仅可以分发图文 还可以分发视频

    image-20240501102924294

  • 聚媒通

    不仅可以分发图文 还可以分发视频

    该平台的优势是无需任何费用,无账号数量限制,纯免费无限制

    image-20240501103108354

  • 融媒宝

    不仅可以分发图文 还可以分发视频

    免费管理五个账号 超出需要收费

    image-20240501104054167

总结

以上的内容分发平台只是众多工具中的一部分 我们在使用这些工具时建议先做好测试 某些自媒体平台可能会对发布来源进行检测 比如头条系 就能检测出你的视频是否为分发平台发布 这种情况下 视频不会被官方推荐

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

使用python3将wordpress博客数据迁移到hexo

发表于 2024-04-16

前言

为了追求网站访问的稳定性 准备将wordpress博客迁移到hexo上 考虑到数据量比较多 我直接将数据库导出成压缩包 然后下载到本地电脑 然后使用python批量将里面的文章转成markdown格式的文档

准备工作

  1. 导出数据库

    1
    mysqldump -u用户名 -p密码 数据库名 | gzip > 数据库名.sql.gz
  2. 将导出的数据库文件下载到本地 然后双击解压该数据库

    1
    scp -r root@服务器IP:刚导出的数据库所在路径 本地目录
  3. 数据库导入进本地mysql中 方便python连接

    首先创建新数据库:

    1
    mysql>create database 数据库名;

    然后将sql文件导入到该库中:

    1
    mysql -u用户名 -p密码 数据库名 < 数据库名.sql

Python批量转换

python代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# coding:utf-8

##########################
#
# 链接本地mysql数据库将wordpress中的文章提取出来并转成hexo博客格式
#
##########################

import os
import sys
from markdownify import markdownify as md
import pymysql.cursors
import sys
sys.setrecursionlimit(1000000)


# 连接数据库
conn = pymysql.Connect(
host='127.0.0.1',
port=3306,
user='数据库用户名',
passwd='数据库密码',
db='wordpress_it',
charset='utf8mb4'
)
# 获取游标
cursor = conn.cursor()


# 获取所有文章
cursor.execute("SELECT post_title, post_content, post_date FROM wp_posts WHERE post_type='post'")
posts = cursor.fetchall()

# 确保输出目录存在
output_dir = 'hexo_posts'
if not os.path.exists(output_dir):
os.makedirs(output_dir)

tags=["Android","IOS","服务器","Docker","Mysql","Flutter","Java","Hexo"]
postTag="技术文章" #默认分类为

# 遍历每篇文章
for idx, post in enumerate(posts):
title, content,date = post
for tag in tags:
if tag in title:
postTag=tag
break


# 将HTML内容转换为Markdown
markdown_content = md(content)
# 写入Hexo格式的Markdown文件
with open(os.path.join(output_dir, f"{idx+1}.md"), 'w', encoding='utf-8') as f:
f.write("---\n")
f.write(f"title: {title}\n") # 如果要嵌入变量 需要加f
f.write(f"date: {date}\n") # 请将日期替换为实际发布日期
f.write(f"tags: {postTag}\n")
f.write("---\n")
f.write("\n")
f.write(markdown_content)


# 关闭游标和连接
cursor.close()
conn.close()

想要正常运行该代码 需要安装pymysql和markdownify两个库:

1
2
pip3 install markdownify
pip3 install pymysql

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

编写一个hexo插件 实现给文章中的外部超链接增加一个中间跳转询问页面

发表于 2024-04-16

需求

我本来想让AI给我写一个hexo插件 如下:

1
2
3
4
5
6
7
8
9
10
11
请写一个hexo插件 要求功能如下:

1. 在执行hexo -g指令时自动将文章中的非本站合法超链接提取替换出来 为每个合法的链接生成独立的跳转页面 独立页面的地址和链接形成固定的对应关系 不会因为新增文章而发生对应链接改变的问题
2. 当用户浏览博客时点击超链接会跳转到一个单独的网页 在这个网页中显示文字和该链接地址并询问用户是否需要跳转到该链接 当用户点击文字下方的跳转按钮则自动跳转到该链接
3. 文章中的链接应该替换成对应已经生成的跳转页面的链接
4. 要求能在`config`文件中自定义需要进行排除的域名 不对排除的域名下的所有链接进行跳转页面的生成 保留原始的直接跳转即可
5. 在`config`文件中增加插件的开启开关
6. 需要过滤掉非合法的超链接 非合法的链接不做处理
7. 跳转页面的UI希望能美观一些 内容居中显示

请给出插件的完整代码

然后AI给我的答案是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
以下是一个满足您需求的Hexo插件的完整代码:

const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
const { URL } = require('url');

const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);

const config = hexo.config.external_link_redirect || {};
const excludedDomains = config.excluded_domains || [];
const redirectPageDir = path.join(hexo.public_dir, 'redirect_pages');
const redirectPageTemplate = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redirect Page</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
}
.container {
margin-top: 100px;
}
.link {
font-size: 18px;
font-weight: bold;
}
.button {
padding: 10px 20px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
}
</style>
</head>
<body>
<div class="container">
<p class="link">%LINK%</p>
<button class="button" onclick="redirectToLink()">Go to Link</button>
</div>
<script>
function redirectToLink() {
window.location.href = '%URL%';
}
</script>
</body>
</html>
`;

hexo.extend.filter.register('before_generate', async function () {
if (!config.enable) {
console.log('External link redirect plugin is disabled.');
return;
}

await createRedirectPages();
});

async function createRedirectPages() {
const posts = hexo.locals.get('posts');
if (!posts || !posts.length) {
return;
}

for (const post of posts.data) {
const content = post.content;
const updatedContent = await processContent(content);
post.content = updatedContent;
}
}

async function processContent(content) {
const updatedContent = content.replace(/<a href="([^"]+)"[^>]*>(.*?)<\/a>/gi, async (match, url, text) => {
if (!isLinkValid(url)) {
return match; // Skip invalid links
}

const redirectPagePath = await generateRedirectPage(url);
const redirectLink = `<a href="${redirectPagePath}">${text}</a>`;
return redirectLink;
});

return updatedContent;
}

function isLinkValid(url) {
if (!url || typeof url !== 'string') {
return false;
}

const parsedUrl = new URL(url);
const domain = parsedUrl.hostname.replace('www.', '');
return !excludedDomains.includes(domain);
}

async function generateRedirectPage(url) {
const redirectUrl = new URL(url);
const domain = redirectUrl.hostname.replace('www.', '');
const redirectPagePath = path.join(redirectPageDir, `${domain}_${Date.now()}.html`);
const pageContent = redirectPageTemplate.replace('%LINK%', url).replace('%URL%', url);

await writeFile(redirectPagePath, pageContent);
return redirectPagePath.replace(hexo.public_dir, '');
}
在这个插件中,我们做了以下几件事情:

在执行hexo -g指令之前,通过before_generate过滤器来自动生成跳转页面。
通过正则表达式遍历文章内容,找到所有超链接,并检查链接是否有效。
对于有效的链接,创建一个跳转页面,并将原始链接替换为跳转页面的链接。
生成的跳转页面是一个HTML文件,包含跳转链接和一个按钮,点击按钮可以跳转到原始链接。
用户可以在Hexo配置文件中设置插件的开启状态、需要排除的域名以及跳转页面模板。
最终生成的跳转页面保存在public/redirect_pages目录下,每个页面都有固定的对应关系,不会因为新增文章而改变链接。

看了一下AI给我写的代码, 可以说是秀的我头疼, 代码可读性实在太差了, 难道是我问的方式不对?🤔

由于ChatGPT免费次数有限 我也懒得去搜寻免费的AIChat, 干脆就自己手动写一个吧

最后调试完成后的插件代码如下:

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
'use strict';

const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');
const crypto = require('crypto');
const { log } = require('console');

const DEFAULT_EXCLUDE_DOMAINS = ['example.com']; // 默认排除的域名列表

hexo.extend.filter.register('before_generate', function () {
const config = hexo.config.external_links || {};
const excludeDomains = config.exclude_domains || DEFAULT_EXCLUDE_DOMAINS;
if (!config.enable) {
console.log('关闭external_links插件');
return;
}
hexo.extend.generator.register('external_links', function (locals) {
const posts = locals.posts;
const author = hexo.config.author;
const outputDir = hexo.config.external_links_output || 'external_links';
const linkMap = new Map(); // 存储链接和其对应的哈希值的映射

posts.forEach(post => {
const $ = cheerio.load(post.content);
$('a').each(function () {
const href = filterUrl($(this).attr('href'));
const hostname = getHostname(href);

if (hostname!=""&&!excludeDomains.includes(hostname)) {
const hash = generateHash(href);
linkMap.set(href, hash); // 存储链接和哈希值的映射关系

//替换文章中的原始链接
const newHref = `/${outputDir}/${hash}.html`;
$(this).attr('href', newHref);
$(this).attr('target', "blank");//在新标签页中打开超链接
}
});

post.content = $.html();
});

const outputPath = path.join(hexo.public_dir, outputDir);
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath);
}

// 生成每个链接对应的页面
linkMap.forEach((hash, href) => {
const htmlContent = generateLinkPage(href, hash,author);
const outputFile = path.join(outputPath, `${hash}.html`);
fs.writeFileSync(outputFile, htmlContent);
});


});
});

function generateLinkPage(href, hash,author) {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>访问外部网站-{author}</title>
<style>
body {
margin: 20px;
padding-top: 100px;
color: #222;
font-size: 13px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.5;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.wrapper {
margin: auto;
padding-left: 30px;
padding-right: 30px;
max-width: 540px;
padding-top: 25px;
padding-bottom: 25px;
background-color: #f7f7f7;
border: 1px solid #babbbc;
border-radius: 5px;
}



.button {
padding: 0;
font-family: inherit;
background: none;
border: none;
outline: none;
cursor: pointer;
}
.button:hover {
background-color: #0070cd;
}
a{
text-decoration: none
}

.button:active {
background-color: #0077d9;
}


.link {
margin-bottom: 10px;
}

.button {
display: inline-block;
padding: 10px 16px;
color: #fff;
font-size: 14px;
line-height: 1;
background-color: #0077d9;
border-radius: 3px;
}
.actions {
margin-top: 15px;
padding-top: 30px;
text-align: right;
border-top: 1px solid #d8d8d8;
}
</style>
</head>
<body>

<div class="wrapper">
<div class="content">
<h1>即将离开${author}</h1>
<p class="info">您即将离开${author},前往外部网站。</p>
<p class="link">${href}</p>
</div>
<div class="actions">
<a class="button" href="${href}" one-link-mark="yes">继续访问</a>
</div>
</div>
</body>
</html>
`;
return html;
}

function getHostname(url) {
try {
return new URL(url).hostname;
} catch (error) {
return '';
}
}
function filterUrl(url) {
const urlRegex = /http[s]?:\/\/[\u4e00-\u9fa5\w.-\/:]+[\u4e00-\u9fa5\w.?&\/=-]+/g;
const match = urlRegex.exec(url);
return match ? match[0] : ''
}

function generateHash(data) {
return crypto.createHash('md5').update(data).digest('hex');
}

使用方法:

在script目录下新建一个js文件, 注意名称不要使用index.js, 将上面代码粘贴过去, 然后在config配置文件中指定输出的目录以及需要过滤的域名 如下:

1
2
3
4
5
6
7
8
9
10
#需要排除的域名 以及是否开启该插件
external_links:
exclude_domains:
- 'acg.newban.cn'
- 'newban.cn'
- 'code.newban.cn'
- 'audio.newban.cn'
enable: true
#输出目录
external_links_output: external_links

效果展示

模仿的知乎效果:

image-20240416141911249

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

数据库常用操作指令

发表于 2024-04-15

前言

数据库操作指令涵盖了创建、查询、修改和删除数据库及表的过程,以及如何在这些数据库和表中进行数据操作。以下是一些常用的数据库操作指令:

指令

创建数据库

1
create database 数据库名;

删除数据库

1
drop database 数据库名;

创建表

1
create table 表名 (列名 数据类型);

删除表

1
drop table 表名;

显示所有数据库

1
show databases;

进入数据库

1
use 数据库名;

显示所有表

1
show tables;

插入数据

1
insert into 表名 values(值1,值2,...);

修改数据

1
update 表名 set 字段=新值 where 条件;

删除数据

1
delete from 表名 where 条件;

修改表名

1
alter table 旧表名 rename to 新表名;

查看表所有的字段

1
describe 表名

或者

1
show columns from 表名

添加字段

1
alter table 表名 add 字段名 数据类型;

修改字段

1
alter table modify 字段名 新数据类型;

删除字段

1
alter table 表名 drop 字段名;

这些指令提供了对数据库和表的基本操作,包括创建、修改、删除和查询数据 根据具体需求,可以组合使用这些指令来完成更复杂的数据库管理任务

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

Mac平台如何下载ffmpeg并对m3u8视频进行下载转换成MP4

发表于 2024-04-13

ffmpeg安装

在Mac平台上 本人多次使用brew工具进行ffmpeg安装都未成功

于是决定直接去ffmpeg官网下载执行包

点击进入官网

由于是国外的网站 下载速度会相对较慢 文末已经给大家准备了网盘下载链接

这里我们选择Mac :

image-20240413173518406

image-20240413173539785

image-20240413173602675

将执行压缩包解压后得到一个独立的ffmpeg执行文件:

image-20240413173929604

为了方便我们接下来的使用 需要对其进行环境变量的配置:

  1. 在终端中输入以下命令 打开环境变量配置文件:

    1
    vim ~/.bash_profile
  2. 在该文件中加入以下内容

    1
    export PATH="/path/to/ffmpeg:$PATH"

    注意: 这里需要将 /path/to/ffmpeg 替换为你的ffmpeg所在路径

  1. 使用source命令使新的环境变量生效

    1
    source ~/.bash_profile
  2. 验证是否配置成功

    在任意一个非ffmpeg所在目录执行以下命令, 如果正常打印内容 则代表配置成功:

    1
    ffmpeg -version

注意: 如果你懒得配置环境变量 还可以直接将ffmpeg执行文件移动至/usr/local/bin 此目录下的所有执行文件 全局可运行

下载m3u8视频并转换成mp4

ffmpeg支持直接访问m3u8链接并将其内容转成mp4格式, 执行命令如下:

1
ffmpeg -i {m3u8链接} -c copy -bsf:a aac_adtstoasc {文件名}.mp4

同样的该ffmpeg指令也适用于直播流的下载和转换 比如http://xxx.flv:

1
ffmpeg -i {直播链接} -c copy -bsf:a aac_adtstoasc {文件名}.mp4

ffmpeg分段写入磁盘

当我们使用ffmpeg -i 直播链接 -c copy xxx.mp4指令来下载直播视频时, 默认会先缓存到内存中, 手动停止下载后才写入到硬盘, 针对大文件或者时间较长的直播并不适用, 容易爆内存

此时我们需要开启分段写入模式, 可分为按时间或者视频大小来自动分段:

1. 按时间自动分段(最常用)

比如每 10 分钟(600秒)自动生成一个新文件,文件名自动编号:

1
2
3
4
5
6
7
ffmpeg -i "你的直播流地址" \
-c copy \
-f segment \
-segment_time 600 \
-reset_timestamps 1 \
-strftime 1 \
"record_%Y%m%d_%H%M%S.mp4"

参数拆解:

  • -c copy:直接复制流,不重新编码,CPU 占用极低,且画质无损。
  • -segment_time 600:关键参数。每 600 秒(10分钟)切分一个文件。
  • -reset_timestamps 1:重置每个分段的时间戳,避免单个文件过大导致播放器卡顿。
  • -strftime 1:使用时间戳命名(如 record_20260604_153045.mp4),方便排序。如果去掉此参数,可以用 %03d数字编号(如 record_001.mp4)。

2. 按文件大小自动分段

如果你更关心磁盘空间,可以限制每个文件的最大体积(比如每 500MB 切一刀):

1
2
3
4
5
6
7
ffmpeg -i "你的直播流地址" \
-c copy \
-f segment \
-segment_time 36000 \
-segment_format mp4 \
-fs 500M \
"record_%03d.mp4"

参数说明:

  • -fs 500M:关键参数。当单个文件达到 500MB 时,立即切分到下一个文件。
  • -segment_time 36000:这是一个保险值(比如设10小时),防止文件一直达不到指定大小导致单个文件过大。实际以 -fs先触发为准。

但是这种模式也存在一个麻烦点, 那就是分段写入完成后, 需要手动合并:

  1. 录制时:FFmpeg 只负责生成 record_001.mp4、record_002.mp4… 这些独立文件。

  2. 合并时:直播结束后,需要使用 FFmpeg 的 concat功能无损合并(不重新编码,速度极快):

    1
    2
    3
    4
    5
    6
    7
    # 1. 先创建一个文件列表 filelist.txt
    # filelist.txt 内容示例:
    # file 'record_001.mp4'
    # file 'record_002.mp4'

    # 2. 执行合并
    ffmpeg -f concat -safe 0 -i filelist.txt -c copy final_video.mp4

自动分段合并

如果你想实现自动合并, 可以考虑使用脚本的形式实现

本地创建auto_record_robust.sh文件, 并将以下代码粘贴进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#!/bin/bash

# 定义变量
OUTPUT_PREFIX="record_$(date +%Y%m%d_%H%M%S)"
FINAL_NAME="final_$(date +%Y%m%d_%H%M%S).mp4"
FILELIST="filelist_${OUTPUT_PREFIX}.txt"

# 信号处理函数:确保脚本退出前执行合并
cleanup() {
echo -e "\n检测到中断信号,开始合并已录制的分段文件..."
merge_segments
exit 0
}

# 合并函数
merge_segments() {
# 检查是否有分段文件
if ls ${OUTPUT_PREFIX}_*.mp4 1> /dev/null 2>&1; then
echo "正在合并分段文件..."

# 生成按修改时间排序的文件列表
ls -1v ${OUTPUT_PREFIX}_*.mp4 > "$FILELIST" 2>/dev/null ||
ls -1 ${OUTPUT_PREFIX}_*.mp4 | sort > "$FILELIST"

# 检查文件数量
FILE_COUNT=$(wc -l < "$FILELIST")
echo "找到 $FILE_COUNT 个分段文件"

if [ $FILE_COUNT -eq 0 ]; then
echo "没有找到可合并的文件"
return
fi

if [ $FILE_COUNT -eq 1 ]; then
# 只有一个文件,直接重命名
echo "只有一个分段文件,直接重命名为最终文件"
mv $(head -1 "$FILELIST") "$FINAL_NAME"
else
# 多个文件,合并
echo "正在合并到 $FINAL_NAME ..."
ffmpeg -f concat -safe 0 -i "$FILELIST" -c copy "$FINAL_NAME" 2> merge.log

if [ $? -eq 0 ]; then
echo "合并成功!最终文件: $FINAL_NAME"
# 可选:删除分段文件
# rm ${OUTPUT_PREFIX}_*.mp4
else
echo "合并失败,请检查 merge.log 文件"
fi
fi

# 清理文件列表
rm -f "$FILELIST"
else
echo "没有找到分段文件,无法合并"
fi
}

# 捕获退出信号
trap cleanup SIGINT SIGTERM SIGHUP EXIT

# 1. 开始分段录制
echo "开始录制直播到 ${OUTPUT_PREFIX}_*.mp4 ..."
echo "按 Ctrl+C 可中断录制并自动合并"

# 启动FFmpeg,不设置timeout
ffmpeg -i "$1" \
-c copy \
-f segment \
-segment_time 600 \
-reset_timestamps 1 \
-movflags faststart \
-avoid_negative_ts 1 \
-fflags +genpts \
"${OUTPUT_PREFIX}_%03d.mp4"

# 获取FFmpeg退出状态
FFMPEG_EXIT=$?

# 判断退出原因
if [ $FFMPEG_EXIT -eq 0 ]; then
echo "FFmpeg正常结束(可能是直播主动结束)"
else
echo "FFmpeg异常退出(代码: $FFMPEG_EXIT),可能是网络中断或错误"
fi

# 无论何种退出,都执行合并
merge_segments

echo "脚本执行完毕"

使用方法

1
2
3
4
5
6
7
# 赋予执行权限
chmod +x auto_record_robust.sh

# 运行
./auto_record_robust.sh "你的直播地址"

# 随时按 Ctrl+C 中断,脚本会自动合并已录制的部分

脚本特点:

  1. 信号捕获:trap cleanup SIGINT SIGTERM SIGHUP EXIT捕获所有中断信号
  2. 任何情况都合并:无论 FFmpeg 正常退出(0)还是异常退出(非0),都会执行合并
  3. 文件存在检查:先检查是否有分段文件,避免空合并
  4. 智能排序:ls -1v按自然顺序排序(如 001, 002, 003)
  5. 日志记录:合并过程的错误会保存到 merge.log

直播文件损坏预防

手动终止下载后, 硬盘写入的视频文件可以正常播放, 如果是因为网络原因或者推流中断导致的下载终止, 那么本地的视频可能会提示损坏无法播放, 这通常是MOOV 元数据没来得及写入导致

为了预防这个问题, 我们可以在命令行中添加-movflags faststart参数来实时更新文件头

这个问题非常典型,完全可以修复。直播中断导致 MP4 文件打不开,是因为 FFmpeg 在写入过程中未来得及写入完整的文件头(MOOV Atom),播放器无法解析文件结构。

为什么中断会导致文件损坏?

MP4 文件的“目录”(MOOV 元数据)通常写在文件的末尾。直播中断时,这个“目录”缺失了,播放器就不知道视频数据在哪里,所以报错。

解决方案:使用 -movflags faststart(首选方案)

这是最推荐的预防性措施。它会让 FFmpeg 在录制过程中实时更新文件头,即使中断,已写入的部分也是可播放的。

修改后的录制命令:

1
2
3
4
5
6
7
ffmpeg -i "你的直播流地址" \
-c copy \
-f segment \
-segment_time 600 \
-reset_timestamps 1 \
-movflags faststart \
"record_%Y%m%d_%H%M%S.mp4"

关键点:

  • 添加了 -movflags faststart参数。
  • 这会将元数据移到文件头部,边录边写,确保每个分段文件随时处于“可播放”状态。
  • 代价是极轻微的 CPU 开销(几乎可忽略),换来的数据安全性极高。

如果已有损坏文件,如何修复?

如果你手头已经有因中断而损坏的 .mp4文件,可以使用以下方法尝试修复:

方法一:使用 FFmpeg 直接转码(无损修复)

1
ffmpeg -i broken_video.mp4 -c copy repaired_video.mp4
  • 原理:FFmpeg 会尝试重新解析数据并生成正确的文件头。
  • 成功率:90% 以上。只要视频数据本身没损坏,通常都能救回来。

方法二:使用 qt-faststart(针对未完成文件)

如果方法一失败,可以尝试专门修复 MOOV 头:

1
2
3
4
5
# 1. 提取 MOOV 信息(如果存在)
qt-faststart broken_video.mp4 temp_fixed.mp4

# 2. 如果上述失败,尝试强制转换
ffmpeg -i broken_video.mp4 -c:v libx264 -c:a aac fixed_video.mp4

(注:qt-faststart是 FFmpeg 工具集的一部分,通常随 FFmpeg 安装)

断线重连

RTMP/RTSP 流:如果流不稳定,建议加上断线重连参数:

1
ffmpeg -reconnect 1 -reconnect_streamed 1 -i "rtmp://..." ...(其余参数同上)

当然我们还可以使用脚本实现无限重试下载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 用 systemd 或 supervisor 监控进程
# 2. 主脚本只管录制
# 3. 独立的清理脚本定时合并旧文件

# 简单示例:无限重试录制
while true; do
ffmpeg -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 \
-i "直播地址" \
-c copy -f segment \
-segment_time 600 \
-reset_timestamps 1 \
-movflags faststart \
"record_%Y%m%d_%H%M%S.mp4"
echo "连接断开,5秒后重试..."
sleep 5
done

终极指令

集断流重连、自动分段、文件头实时更新于一体的终极指令:

1
2
3
4
5
6
7
8
9
ffmpeg -hide_banner -stats \
-reconnect 1 -reconnect_delay_max 30 \
-i "你的直播地址" \
-c copy \
-f segment -segment_time 3600 \
-reset_timestamps 1 \
-movflags faststart+empty_moov \
-avoid_negative_ts make_zero \
"录制_%Y%m%d_%H%M%S.mp4"

针对不同协议优化版

终极脚本

集断流重连、自动分段、实时修复、资源控制、自动合并于一体的终极脚本,支持多种直播协议,经过生产环境验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
#!/bin/bash
# live_recorder.sh - 基础重连自动合并脚本

set -euo pipefail # 严格错误处理

# 配置变量
OUTPUT_PREFIX="${2:-live}"
FINAL_OUTPUT="${4:-completed}"
LOG_DIR="${3:-./logs}"
LOCK_FILE="/tmp/live_recorder_$$.lock"
FFMPEG_PID=""

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# 优雅停止 FFmpeg
stop_ffmpeg_gracefully() {
local pid=$1
if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
return
fi

echo -e "${BLUE}[*] 正在优雅停止 FFmpeg (PID: $pid)...${NC}"

# 先发送 SIGINT 让 FFmpeg 优雅结束
kill -INT "$pid" 2>/dev/null || true

# 等待最多 5 秒
local timeout=5
while [ $timeout -gt 0 ] && kill -0 "$pid" 2>/dev/null; do
sleep 1
((timeout--))
done

# 如果还在运行,发送 SIGTERM
if kill -0 "$pid" 2>/dev/null; then
echo -e "${YELLOW}[!] FFmpeg 未响应,强制停止...${NC}"
kill -TERM "$pid" 2>/dev/null || true
sleep 2
fi

# 如果仍然在运行,强制杀死
if kill -0 "$pid" 2>/dev/null; then
echo -e "${RED}[!] FFmpeg 仍在运行,强制杀死...${NC}"
kill -KILL "$pid" 2>/dev/null || true
fi

# 确保进程结束
wait "$pid" 2>/dev/null || true
}

# 修复损坏的 MP4 文件
repair_mp4_file() {
local file="$1"
if [ ! -f "$file" ]; then
return 1
fi

# 检查文件是否损坏
if ffprobe -v error -i "$file" 2>/dev/null; then
echo -e "${GREEN}[✓] 文件正常: $(basename "$file")${NC}"
return 0
fi

echo -e "${YELLOW}[!] 修复损坏文件: $(basename "$file")${NC}"

# 尝试修复
local temp_file="${file}.fixed.mp4"
local log_file="${LOG_DIR}/repair_$(basename "$file").log"

# 方法1: 使用 FFmpeg 重新编码
if ffmpeg -hide_banner -err_detect ignore_err -i "$file" \
-c copy -movflags faststart "$temp_file" 2>"$log_file"; then
mv "$temp_file" "$file"
echo -e "${GREEN}[✓] 文件修复成功${NC}"
return 0
fi

# 方法2: 尝试使用 qt-faststart
if command -v qt-faststart &>/dev/null; then
if qt-faststart "$file" "$temp_file" 2>/dev/null; then
mv "$temp_file" "$file"
echo -e "${GREEN}[✓] 使用 qt-faststart 修复成功${NC}"
return 0
fi
fi

# 方法3: 尝试移除损坏部分
local size=$(stat -f %z "$file" 2>/dev/null || stat -c %s "$file" 2>/dev/null)
if [ "$size" -gt 1000000 ]; then # 文件大于 1MB
# 尝试裁剪最后 1MB(可能是未写完的部分)
local new_size=$((size - 1000000))
if [ "$new_size" -gt 100000 ]; then # 确保还有足够大小
head -c "$new_size" "$file" > "$temp_file" 2>/dev/null
mv "$temp_file" "$file"
echo -e "${YELLOW}[!] 已裁剪文件末尾${NC}"
return 0
fi
fi

echo -e "${RED}[✗] 无法修复文件: $(basename "$file")${NC}"
return 1
}

# 信号处理函数
cleanup() {
echo -e "\n${YELLOW}[!] 收到中断信号,正在清理...${NC}"

# 停止 FFmpeg
if [ -f "$LOCK_FILE" ]; then
FFMPEG_PID=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
if [ -n "$FFMPEG_PID" ] && kill -0 "$FFMPEG_PID" 2>/dev/null 2>/dev/null; then
stop_ffmpeg_gracefully "$FFMPEG_PID"
fi
rm -f "$LOCK_FILE"
fi

# 等待一会儿让文件系统同步
sleep 1

# 修复可能损坏的最后一个文件
repair_last_segment

# 执行合并
merge_recordings

echo -e "${GREEN}[✓] 清理完成,脚本退出${NC}"
exit 0
}

# 修复最后一个分段文件
repair_last_segment() {
local files=($(ls -1t "${OUTPUT_PREFIX}_"*.mp4 2>/dev/null | head -5))

if [ ${#files[@]} -eq 0 ]; then
return
fi

echo -e "${BLUE}[*] 检查并修复分段文件...${NC}"

for file in "${files[@]}"; do
repair_mp4_file "$file"
done
}

# 合并函数
merge_recordings() {
# 先确保所有文件都已关闭
sleep 2

local files=("${OUTPUT_PREFIX}_"*.mp4)

if [ ${#files[@]} -eq 0 ] || [ ! -f "${files[0]}" ]; then
echo -e "${YELLOW}[!] 没有找到分段文件可合并${NC}"
return
fi

# 确保文件可读
echo -e "${BLUE}[*] 验证文件完整性...${NC}"
local valid_files=()
for file in "${files[@]}"; do
if repair_mp4_file "$file"; then
valid_files+=("$file")
fi
done

if [ ${#valid_files[@]} -eq 0 ]; then
echo -e "${RED}[✗] 没有可用的有效文件${NC}"
return
fi

local file_count=${#valid_files[@]}
echo -e "${BLUE}[*] 找到 $file_count 个有效文件${NC}"

if [ $file_count -eq 1 ]; then
echo -e "${BLUE}[*] 只有一个文件,直接重命名${NC}"
local new_name="${FINAL_OUTPUT}/$(basename "${valid_files[0]}" .mp4)_full.mp4"
cp "${valid_files[0]}" "$new_name"
echo -e "${GREEN}[✓] 保存为: $new_name${NC}"
return
fi

# 创建文件列表(按修改时间排序)
local file_list="${LOG_DIR}/concat_list_$$.txt"

# 按修改时间排序
for file in "${valid_files[@]}"; do
echo -e "$(stat -f %m "$file" 2>/dev/null || stat -c %Y "$file" 2>/dev/null)\t$file"
done | sort -n | cut -f2- > "$file_list"

# 创建合并列表
local merge_list="${file_list}.txt"
while IFS= read -r file; do
if [ -f "$file" ] && ffprobe -v error "$file" 2>/dev/null; then
echo "file '$file'" >> "$merge_list"
else
echo -e "${YELLOW}[!] 跳过无效文件: $file${NC}"
fi
done < "$file_list"

if [ ! -f "$merge_list" ] || [ ! -s "$merge_list" ]; then
echo -e "${RED}[✗] 没有有效的合并列表${NC}"
rm -f "$file_list" "$merge_list" 2>/dev/null || true
return
fi

local valid_count=$(wc -l < "$merge_list")
if [ "$valid_count" -eq 0 ]; then
echo -e "${RED}[✗] 没有可合并的有效文件${NC}"
rm -f "$file_list" "$merge_list" 2>/dev/null || true
return
fi

if [ "$valid_count" -eq 1 ]; then
echo -e "${BLUE}[*] 只有一个有效文件,直接保存${NC}"
local single_file=$(head -1 "$merge_list" | sed "s/^file '//;s/'$//")
local new_name="${FINAL_OUTPUT}/$(basename "$single_file" .mp4)_full.mp4"
cp "$single_file" "$new_name"
echo -e "${GREEN}[✓] 保存为: $new_name${NC}"
rm -f "$file_list" "$merge_list" 2>/dev/null || true
return
fi

# 开始合并
local timestamp=$(date +%Y%m%d_%H%M%S)
local final_file="${FINAL_OUTPUT}/${OUTPUT_PREFIX}_full_${timestamp}.mp4"
local log_file="${LOG_DIR}/merge_${timestamp}.log"

echo -e "${BLUE}[*] 开始合并 $valid_count 个文件 -> $final_file${NC}"

# 显示要合并的文件
echo -e "${BLUE}[*] 合并列表:${NC}"
cat -n "$merge_list" | sed "s/file '//;s/'$//" | while read -r num file; do
local size=$(du -h "$file" 2>/dev/null | cut -f1 || echo "未知")
echo -e " ${num}. $(basename "$file") (${size})"
done

# 执行合并
ffmpeg -hide_banner \
-f concat \
-safe 0 \
-i "$merge_list" \
-c copy \
-movflags faststart \
-max_muxing_queue_size 9999 \
"$final_file" 2> "$log_file"

if [ $? -eq 0 ]; then
echo -e "${GREEN}[✓] 合并成功: $final_file${NC}"
local final_size=$(du -h "$final_file" 2>/dev/null | cut -f1 || echo "未知")
echo -e "${BLUE}[*] 最终文件大小: ${final_size}${NC}"

# 验证最终文件
if ffprobe -v error "$final_file" 2>/dev/null; then
local duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$final_file" 2>/dev/null | cut -d. -f1)
if [ -n "$duration" ]; then
local hours=$((duration / 3600))
local minutes=$(((duration % 3600) / 60))
local seconds=$((duration % 60))
echo -e "${BLUE}[*] 视频时长: ${hours}:$(printf "%02d" ${minutes}):$(printf "%02d" ${seconds})${NC}"
fi
fi

# 询问是否删除分段文件
echo
read -p "是否删除原始分段文件?(y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -f "${OUTPUT_PREFIX}_"*.mp4
echo -e "${GREEN}[✓] 已删除分段文件${NC}"
else
echo -e "${BLUE}[*] 保留分段文件${NC}"
fi
else
echo -e "${RED}[✗] 合并失败,查看日志: $log_file${NC}"
echo -e "${YELLOW}[!] 合并失败,尝试备用方法...${NC}"

# 尝试使用 mkvmerge 合并(如果有)
if command -v mkvmerge &>/dev/null; then
echo -e "${BLUE}[*] 尝试使用 mkvmerge 合并...${NC}"
local mkv_file="${final_file%.mp4}.mkv"
mkvmerge -o "$mkv_file" $(cat "$merge_list" | sed "s/file '//;s/'$//" | tr '\n' ' ') 2>/dev/null
if [ $? -eq 0 ]; then
echo -e "${GREEN}[✓] mkvmerge 合并成功: $mkv_file${NC}"
# 转换为 mp4
ffmpeg -i "$mkv_file" -c copy "${mkv_file%.mkv}.mp4" 2>/dev/null
if [ $? -eq 0 ]; then
echo -e "${GREEN}[✓] 已转换为 MP4 格式${NC}"
rm -f "$mkv_file"
fi
fi
fi
fi

rm -f "$file_list" "$merge_list" 2>/dev/null || true
}

# 检查依赖
check_dependencies() {
if ! command -v ffmpeg &>/dev/null; then
echo -e "${RED}[✗] 错误: 未找到 ffmpeg${NC}"
echo "请安装: apt install ffmpeg 或 brew install ffmpeg"
exit 1
fi

if ! command -v ffprobe &>/dev/null; then
echo -e "${YELLOW}[!] 警告: 未找到 ffprobe,文件验证功能受限${NC}"
fi
}

# 主程序
main() {
# 检查参数
if [ -z "$1" ]; then
echo -e "${BLUE}用法: $0 <直播URL> [输出前缀] [日志目录] [输出目录]${NC}"
echo -e "${BLUE}示例: $0 'https://example.com/live.m3u8' 'my_live' './logs' './videos'${NC}"
echo -e "${BLUE}快捷键: Ctrl+C 停止录制并自动合并${NC}"
exit 1
fi

# 创建目录
mkdir -p "$LOG_DIR" "$FINAL_OUTPUT"

# 检查依赖
check_dependencies

# 注册信号处理
trap cleanup SIGINT SIGTERM SIGQUIT SIGHUP EXIT

echo -e "${GREEN}[✓] 开始录制直播${NC}"
echo -e "${BLUE}[*] 直播URL: $1${NC}"
echo -e "${BLUE}[*] 输出前缀: $OUTPUT_PREFIX${NC}"
echo -e "${BLUE}[*] 日志目录: $LOG_DIR${NC}"
echo -e "${BLUE}[*] 最终输出: $FINAL_OUTPUT${NC}"
echo -e "${BLUE}[*] 按 Ctrl+C 停止录制并自动合并${NC}"
echo -e "${BLUE}[*] 开始时间: $(date)${NC}"
echo -e "${BLUE}[*] 分段文件: ${OUTPUT_PREFIX}_YYYYMMDD_HHMMSS_NNN.mp4${NC}"

# 录制直播
ffmpeg -hide_banner -loglevel warning -stats \
$(echo "$1" | grep -q "\.m3u8" && echo "-http_persistent 1 -http_multiple 1") \
-reconnect 1 -reconnect_streamed 1 -reconnect_at_eof 1 \
-reconnect_on_network_error 1 -reconnect_on_http_error 403,404,4xx,5xx \
-reconnect_delay_max 30 \
-timeout 30000000 \
-user_agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
-headers "Referer: ${1%%/*}" \
-headers "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
-i "$1" \
-c copy -copy_unknown -map 0 \
-f segment -segment_time 3600 -segment_format mp4 \
-segment_atclocktime 1 -strftime 1 \
-reset_timestamps 1 -avoid_negative_ts make_zero \
-movflags faststart+empty_moov+separate_moof+omit_tfhd_offset \
-write_tmcd 0 -flush_packets 1 \
-max_muxing_queue_size 1024 -max_interleave_delta 0 \
-fflags +genpts+discardcorrupt+igndts \
-use_wallclock_as_timestamps 1 \
"${OUTPUT_PREFIX}_%Y%m%d_%H%M%S_%03d.mp4" &

FFMPEG_PID=$!
echo $FFMPEG_PID > "$LOCK_FILE"

# 显示进度
echo -e "${GREEN}[✓] FFmpeg 进程已启动 (PID: $FFMPEG_PID)${NC}"
echo -e "${BLUE}[*] 录制中... 按 Ctrl+C 停止${NC}"

# 监控进程
while kill -0 "$FFMPEG_PID" 2>/dev/null; do
sleep 5
# 显示当前文件大小
local latest_file=$(ls -t "${OUTPUT_PREFIX}_"*.mp4 2>/dev/null | head -1)
if [ -n "$latest_file" ] && [ -f "$latest_file" ]; then
local size=$(du -h "$latest_file" 2>/dev/null | cut -f1 || echo "0")
echo -ne "\r${BLUE}[*] 当前分段: $(basename "$latest_file") (${size}) 录制中...${NC} "
fi
done

# 清理锁文件
rm -f "$LOCK_FILE" 2>/dev/null

echo -e "\n${BLUE}[*] FFmpeg 进程已结束${NC}"
echo -e "${BLUE}[*] 结束时间: $(date)${NC}"

# 检查是否有文件需要合并
local file_count=$(ls -1 "${OUTPUT_PREFIX}_"*.mp4 2>/dev/null | wc -l)
if [ "$file_count" -gt 0 ]; then
echo -e "${BLUE}[*] 发现 $file_count 个分段文件,开始合并...${NC}"
merge_recordings
else
echo -e "${YELLOW}[!] 没有录制到任何文件${NC}"
fi

echo -e "${GREEN}[✓] 录制任务完成${NC}"
}

# 执行主程序
main "$@"

分解说明

  1. 输入连接与重连
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 自动识别 HLS
$(echo "$1" | grep -q "\.m3u8" && echo "-http_persistent 1 -http_multiple 1")

# 全方位重连机制
-reconnect 1 -reconnect_streamed 1 -reconnect_at_eof 1
-reconnect_on_network_error 1 -reconnect_on_http_error 4xx,5xx
-reconnect_delay_max 30 # 最大重连延迟30秒

# 超时控制
-timeout 30000000 # 30秒连接超时
-stimeout 10000000 # 10秒流读取超时

# 伪装请求
-user_agent "Mozilla/5.0..."
-headers "Referer: https://www.example.com\r\n"
  1. 分段录制核心
1
2
3
4
5
6
7
8
9
10
11
# 无损复制
-c copy -copy_unknown -map 0

# 分段参数
-f segment -segment_time 3600 # 每小时一个文件
-segment_format mp4
-segment_atclocktime 1 # 整点分割
-strftime 1 # 时间戳命名

# 命名格式:live_20240604_153000_001.mp4
# 分别对应:前缀_年月日_时分秒_序号
  1. 容错与修复
1
2
3
4
5
6
7
8
9
10
11
# 时间戳处理
-reset_timestamps 1
-avoid_negative_ts make_zero
-use_wallclock_as_timestamps 1
-fflags +genpts+igndts+discardcorrupt+nobuffer

# MP4修复三件套
-movflags faststart+empty_moov+separate_moof+omit_tfhd_offset
# faststart: 实时更新文件头
# empty_moov: 防止中断损坏
# separate_moof: 增强流媒体兼容性
  1. 性能优化
1
2
3
4
-max_muxing_queue_size 1024  # 增大缓冲队列
-max_interleave_delta 0 # 禁用交错延迟
-write_tmcd 0 # 禁用不必要的时间码
-flush_packets 1 # 及时刷入磁盘

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 保存脚本
nano live_recorder.sh
# 粘贴上面的内容
# 按 Ctrl+X, 输入 Y, 按 Enter 保存

# 2. 赋予执行权限
chmod +x live_recorder.sh

# 3. 使用脚本
./live_recorder.sh "直播URL"
# 或带参数
./live_recorder.sh "https://example.com/live.m3u8" "my_live" "./logs" "./videos"
# 参数1 参数2 参数3 参数4
# 脚本自身 直播地址 输出前缀 日志目录 输出目录

# 4. 按 Ctrl+C 停止录制,脚本会自动合并

核心特性

✅ 自动合并功能

  1. 任何中断自动合并 - 无论正常结束、Ctrl+C、信号终止、进程被杀
  2. 智能文件检测 - 自动验证文件完整性,跳过损坏文件
  3. 多种排序策略 - 按修改时间/文件名智能排序
  4. 合并前验证 - 使用 ffprobe检查文件是否可读
  5. 备用合并方案 - 主合并失败时尝试逐段转换再合并

ffmpeg国内下载

网盘下载 (访问密码: 312306)

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

使用ffmpeg将本地的flv视频压制转换成小体积的MP4

发表于 2024-04-13

前言

关于ffmpeg的下载, 可查阅上一篇文章《Mac平台如何下载ffmpeg并对m3u8视频进行下载转换成MP4》

本文将给大家介绍如何使用ffmpeg将本地的flv视频压制转换成小体积的MP4

格式转换

如果只是单纯的进行格式转换 可以直接使用以下指令来实现:

1
ffmpeg -i input.flv -vcodec copy -acodec copy output.mp4

这种方式通常体积不会有太大的改变

转换格式并压缩视频体积

如果需要将视频进行压缩 以减少空间的占用 可以参考以下指令:

1
ffmpeg -i input.flv -c:v libx264 -crf 26 -c:a aac -strict experimental -b:a 128k output.mp4

解释各选项含义:

  • -i input.flv:指定输入文件名为input.flv。
  • -c:v libx264:使用H.264编码来压缩视频。
  • -crf 28:设置CRF(常量速率因子)的值为23。低值表示更好的质量,但文件会更大;高值会降低质量。范围是从0(无损)到51(最糟),通常使用18到28, 27通常是用于MP4格式的视频质量很好的平衡
  • -c:a aac:使用AAC编码来压缩音频。
  • -strict experimental:允许使用实验性的编码器。
  • -b:a 128k:设置音频比特率为128k。根据需求可以调整大小。

我们需要找到一个合适的参数平衡点 确保视频画面的质量的同时尽可能地压缩视频体积

如果我们想要快速压制 减少等待时间 还可以指定编码器预设 如下:

1
ffmpeg -i input.flv -c:v libx264 -preset veryfast -crf 24 -c:a aac -b:a 128k output.mp4

解释选项含义:

  • -preset veryfast:指定编码器预设,veryfast提供快速但质量较低的编码, 除此之外 还有fast、HQ、 SuperHQ等预设。

ffmpeg 截取视频

要使用FFmpeg截取视频 可以使用以下指令:

1
ffmpeg -i input.mp4 -ss 00:00:10 -t 00:00:30 -c copy output.mp4

解释各选项含义:

  • -i input.mp4 指定输入视频文件。
  • -ss 00:00:10 表示从视频的指定时间点开始截取,这里是从10秒处开始。
  • -t 00:00:30 表示截取的时长,这里是30秒。
  • -c copy 表示复制编码器,即不重新编码,直接复制视频流和音频流。
  • output.mp4 是输出文件的名称。

请根据实际需求调整时间参数。如果需要不同的起始时间或时长,只需修改-ss和-t参数即可。如果需要对视频进行重编码,可以去掉-c copy参数或指定其他编码器选项。

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

如何开发hexo插件

发表于 2024-04-13

前言

Hexo 有强大的插件系统,使您能轻松扩展功能而不用修改核心模块的源码。在 Hexo 中有两种形式的插件:

脚本(Scripts)

如果您的代码很简单,建议您编写脚本,您只需要把 JavaScript 文件放到 scripts 文件夹,在启动时就会自动加载。

插件(Packages)

如果您的代码较复杂,或是您想要发布到 NPM 上,建议您编写插件。首先,在 node_modules 文件夹中建立文件夹,文件夹名称开头必须为 hexo-,如此一来 Hexo 才会在启动时加载;否则 Hexo 将会忽略它。

文件夹内至少要包含 2 个文件:一个是主程序,另一个是 package.json,描述插件的用途和所依赖的插件。

1
2
3
.
├── index.js
└── package.json

package.json 中至少要包含 name, version, main 属性,例如:

1
2
3
4
5
package.json{
"name": "hexo-my-plugin",
"version": "0.0.1",
"main": "index"
}

工具

您可以使用 Hexo 提供的官方工具插件来加速开发:

  • hexo-fs:文件 IO
  • hexo-util:工具程式
  • hexo-i18n:本地化(i18n)
  • hexo-pagination:生成分页数据

发布

当您完成插件后,可以考虑将它发布到 插件列表,让更多人能够使用您的插件。发布插件的步骤和 更新文档 非常类似。

  1. Fork hexojs/site

  2. 把库(repository)复制到电脑上,并安装所依赖的插件。

    1
    2
    3
    $ git clone https://github.com/<username>/site.git
    $ cd site
    $ npm install
  3. 在 source/_data/plugins/ 中创建一个新的 yaml 文件,使用您的插件名称作为文件名。

  4. 编辑 source/_data/plugins/<your-plugin-name>.yml 并添加您的插件。例如:

    1
    2
    3
    4
    5
    6
    description: Server module for Hexo.
    link: https://github.com/hexojs/hexo-server
    tags:
    - official
    - server
    - console
  5. 推送(push)分支。

  6. 建立一个新的合并申请(pull request)并描述改动。

本文转载自 : Hexo中文网

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

关于群晖NAS使用DownloadStation下载直播流的问题以及缓存目录挂载

发表于 2024-04-12

前言

最近有群晖NSA下载直播流的需求 在使用DownloadStation套件的时候遇到一些坑 在这里做个记录

关于流的下载

首先 我需要下载的流地址的格式为:http://xxxx.flv

如果我使用常规添加链接地址的方式进行下载 就容易出现处于一直读取保存状态:

image-20240412083406987

这种状态下我们无法关闭应用 只能在套件中心停用该应用才能解决 否则后台会持续地从网络拉取数据:

image-20240412084021153

问题出在于 该地址输入框更适合填入种子的下载链接而不是视频或者流本身的 虽然这样也能变相地下载视频 但是极度不推荐 很多新手在使用的时候一不注意就掉了坑 我就是其中一个😂

正确的使用方式是将视频或者流地址填入搜索框然后回车:

image-20240412085002263

image-20240412085055724

此时视频就能正常下载了

可接下来新的问题又出现了 下载过程中无法在下载目录看到当前下载的文件 只有当视频下载完成或者手动终止该下载任务后才能看到

很明显Download Station有个下载缓存目录 但是这个缓存目录我们在File Station中看不到 只能通过ssh在终端查看

由于我下载的是直播流视频 为了保证视频的完整性 我不能中断视频的下载 在这种情况下 如果要实时查看并当前下载的内容 方便随时播放 那么我需要将隐藏的缓存目录给显示到File Station中

这里 我们需要使用目录的挂载功能

缓存目录的挂载

Download Station默认的缓存目录是/volume/@download 如果我们要将其挂载到指定的目录比如homes/download_cache 那么需要先在File Station中手动创建该文件夹

注意: 这里不建议使用终端命令进行创建 因为很容易创建隐藏文件

接下来 按照以下步骤进行缓存目录的挂载

  1. 首先在电脑上打开我们的终端窗口 使用ssh连接我们的群晖NAS:

    1
    ssh 你的群晖登录用户名@群晖ip -p22

    回车输入登录密码 即可进入到我们的NAS

  2. 然后使用mount指令将隐藏的缓存目录@download挂载到download_cache 如下:

    1
    sudo mount --bind /volume/@download /volume/home/download_cache
  3. 挂载完毕后 我们会在File Station中找到该目录 并查看到当前正在下载的视频

    image-20240412094619148

    image-20240412094719618

种子缓存目录

上面我们提到 在种子链接输入框填入视频或流的下载地址 也可下载视频 但是这种方式是不推荐的

如果你不小心误操作了 或者也想体验一把 那么坑定也想知道这种方式下载的视频缓存目录在到底在哪个位置

这里 我已经给大家提前探查明白了 这个种子的缓存目录就在volume/@tmp下的btdl文件夹中

如果你想查看这里面存放的数据首先需要修改以下目录权限:

1
sudo chomd 777 volume/@tmp/btdl

然后我们进入该目录会发现一个upload开头的文件 这个文件就是视频或种子文件:

image-20240412103745114

如果是视频文件 那么我们只需要将其拷贝出来修改一下文件后缀名就行

同样的 为了方便在File Station中查看并编辑该文件 我将这个种子缓存目录挂载到home中

操作步骤和上面的大同小异:

  1. 在Flie Station中手动创建一个目录 用于种子缓存目录的映射 我这里将其命名为temp

  2. 使用mount指令将目录进行挂载

    1
    sudo mount --bind /volume/@tmp /volume/home/temp
  3. 挂载完后 我们就能在File Station中找到该目录以及对应的文件

    image-20240412103447862

    由于我个文件是个flv的视频 所以我将其后缀改为.flv

需要注意的是:

假如你的群晖NAS设有多个存储空间 你若把Download Station套件安装到了volume1中 但是下载目录却指定在了volume2下

那么 种子缓存目录需要在套件安装磁盘中也就是volume1里面找 @tmp 目录跟随的是套件

而下载缓存目录则是和下载目录同一个磁盘也就是volume2里面找 @download跟随的是下载目录

关于Download Station使用感受

总体还是比较满意的 下载的稳定性不错

大部分下载软件都不支持流的断点续传 然而Download Station却能做到这一点 值得一赞

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

使用Wireshark对网络进行抓包

发表于 2024-04-11

软件介绍

Wireshark是一款非常强大的跨平台抓包工具 支持Mac和Windows平台

image-20240411201058820

image-20240411201029770

软件下载

点击进入官网

软件配置

这里以Mac平台结合谷歌浏览器为例进行配置

  1. 进入谷歌浏览器设置页 选择启动时打开新的标签页

    image-20240411184036016

  2. 创建keylog.txt用于存储ssl密钥 方便https抓包

    1
    2
    cd /Users/xxx
    touch keylog.txt

    注意:keylog.txt的存放位置由你自己决定 这里我存放到Users目录下

  3. 执行以下指令启动谷歌浏览器

    1
    sudo /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --ssl-key-log-file=/Users/xxx/keylog.txt

    --ssl-key-log-file : 表示将ssl密钥输出到指定文件中

    注意:

    1. 由于我将keylog.txt文件放在了用户目录下 所以指令执行必须加sudo表示以管理者的身份运行

    2. 如果你的谷歌浏览器并未安装在Applications目录下 可以利用以下指令来获取Chrome应用的路径

      1
      sudo find / -iname "Google Chrome"

      image-20240411185614773

  4. 在新打开的浏览器中输入你需要进行https抓包的网址 并回车

    这里我以百度官网https://baidu.com为例

  5. 此时我们会发现keylog.txt文件中已经有了相应的日志信息

    image-20240411185017352

  6. 启动Wireshark 选择需要监控的网卡

    image-20240411200537770

  7. 并配置TLS 关联keylog文件

    image-20240411185810657

    image-20240411185830566

    image-20240411190033618

至此 软件配置完毕 接下来开始抓包测试

抓包测试

  1. 打开百度网页 然后按F12进入Chrome开发者工具 获取百度的实际ip地址 用于过滤使用

    image-20240411190618555

  2. 在Wireshark 过滤栏中输入ip.dst==36.155.132.76 and http对该ip进行过滤

软件详细使用

利用好过滤

Wireshark的快速上手的技巧那就是使用过滤

想要在大量的数据中获取到自己想要的数据 那就必须利用好过滤关键字

我们进行网络抓包时大部分都是抓取http/https请求 因此我们只需在过滤栏中输入http关键字:

image-20240411195505450

当我们在过滤栏进行输入时Wireshark会有自动补全提示 当过滤栏背景为绿色表示软件能识别并根据该关键字进行数据过滤 如果背景色为红色 那表明无法根据该关键字进行过滤:

image-20240411195710102

过滤json数据

通常我们可能会抓取一下应用的api 返回的数据基本都是json格式 此时我们可以在过滤栏输入http and json关键字

image-20240411195944621

过滤图片

如果要过滤图片 可以使用http.content_type 比如:

1
http.content_type =="image/png"

不过为了更好地过滤出数据 我推荐大家使用contains的形式而不是== 这样容错率能大幅提升

1
http.content_type contains "image"

image-20240411224905514

过滤音视频

如果是amr格式的音频可以在过滤栏输入http and amr关键字

如果是MP4格式视频可以在过滤栏输入http and mp4关键字

如果是直播流 可以尝试输入rtp关键字

当然我们还可以通过模糊过滤的方式 比如:

1
http.content_type contains "video"

和

1
http.content_type contains "audio"

image-20240411225240083

根据ip地址过滤

这里需要注意的是 一个完整的数据交互是包括请求和响应两部分的

如果我们需要抓请求的部分 那么使用dst关键字:

1
ip.dst==服务器ip地址

如果我们需要抓响应的的部分 那么使用src关键字 也就是来源于对方 向我们发送数据:

1
ip.src==服务器ip地址

如果请求和响应两个部分都想抓取 那就用addr关键字:

1
ip.addr==服务器ip地址

也就是说无论是Suorce还是Destination只要包含该ip的都会过滤出来

根据请求方法过滤

如果我们要指定过滤GET或者POST方法 那么可以使用以下关键字:

1
http.request.method==GET

根据请求路径进行过滤

有时候同一个应用会使用不同的ip地址 尤其是某些直播应用 虽然每个视频ip可能会不同 但是地址路径是一样的 比如以live/xxx.flv结尾 那么我们就可以根据这个进行匹配过滤 如下:

1
http.request.uri contains "live"

根据排除法进行过滤

如果我们要排除某个ip的数据, 我们可以在关键字前面加上not或者! 如下:

1
!(ip.addr == 192.168.0.1)

根据内容长度进行过滤

如果我们要根据请求的内容进行过滤可以使用http.content_length 关键字 如下:

1
http.content_length <=30

根据正则进行匹配过滤

如果我们要使用正则匹配 可以使用matches关键字 如下:

1
http.request.uri matches "user"

注意: matches匹配不区分大小写

根据域名进行过滤

如果需要根据域名进行过滤 可以使用http.host关键字 如下:

1
http.host==baidu.com

或者

1
http.host contains "baidu"

如果要按照源地址和目标地址过滤 可以使用src www.baidu.com和dst www.baidu.com

根据cookie进行过滤

使用http.cookie关键字 如下:

1
http.cookie contains "userid"

根据端口号过滤

使用http.port关键字 如下:

1
http.port==80

根据http响应状态码过滤

可以使用http.response.code关键字 如下:

1
http.response.code==200

结合使用

以上所有的关键字 都可以使用与或非运算符进行结合使用 比如我想过滤一个http请求且请求响应为403的数据 过滤方式如下:

1
http and (http.response.code==403)

关于清除界面数据

当我们感觉数据过多 需要清理界面数据时 需要停止抓包 然后再点击开始抓包 这个时候软件会给予提示 是否需要将数据包进行保存到本地 我们只需要点不保存即可:

image-20240411200226017

或者直接点击重新抓包一步到位:

image-20240411212025725

Wireshark 的优势

  • 能抓取物理层到应用层七大层数据 这是其他诸如Charles、Fiddler抓包软件所不具备的

  • 监控网卡 所有数据无所遁形

  • 有些App禁止代理状态下使用 或者内部走的非代理流量 那么这种问题Wireshark可以轻松解决

总之 Wireshark和其它抓包软件结合使用 基本上能解决我们的抓包需求

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

1…456…51

乱码三千

android程序员一枚,擅长java,kotlin,python,金融投资,欢迎交流~

507 日志
145 标签
RSS
© 2026 乱码三千
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%