由于前段想学习做切片视频,一方面担心用别人上传的 B站 录播会压缩画质,另一方面也没有每次蹲直播间打开电脑进行录播的条件,所以使用服务器进行自动化录制就显得很是必要。

而据了解 BILIUP 集自动化录制 + 投稿功能于一身,用着还算顺手。但是经实测,使用 BILIUP 进行 Twitch 平台的直播流录制时,很容易会拿到低清晰度的直播流,同时还经常出现在意料之外的地方进行分段,导致录制的质量很难保证。而使用 StreamCap 进行 Twitch 平台的直播流录制就不会出现类似情况,但是 StreamCap 是不带投稿模块的。

本文章展示的是使用 StreamCap 进行 Twitch 平台的直播流录制,同时通过 StreamCap自定义脚本执行命令调用 BILIUP 命令行进行自动投稿至 B站。同时也可以使用 BILIUP 进行 B站 的直播流录制和自动投稿。

一、服务器的选择

1.1、硬盘大小

根据平时使用 StreamCap 进行直播流录制的情况,一般1080P 60帧 6Mbps的直播流,连续录制两个小时所占用的存储空间约为5.2G

手动计算为:
2h 的总字节数:6 * 1000kbps * 7200s = 432 * 10^5kb
转成KB:432 * 10^5kb / 8 = 54 * 10^5KB
转成GB:54 * 10^5KB / 1024^2 ≈ 5.15GB

那么,根据主播常播的时长,以及可能同时录制的直播间个数,就可以大致估算出需要的硬盘空间大小。

  • 1-2个主播,建议至少选择20GB或以上规格的服务器。
  • 3-5个主播,建议至少选择50GB或以上规格的服务器。
  • 6-10个主播,建议至少选择100GB或以上规格的服务器。

注:有的主播的单次直播时长不是固定的,如果单个主播可能连续直播超过12h,那么至少需要12/2 * 5.2GB + 5GB盈余硬盘空间,即应选择40GB或以上硬盘规格的服务器。

1.2、带宽选择

需要自己实际测试,一般国内服务器带宽较小,带宽成本较高,不太建议做录播服务器。除非只录制1-2个直播间。
比如可能需要同时录制3个直播间,那么保险起见应选择(6Mbps * 3 = 18Mbps)20Mbps或以上带宽的服务器。
然而,vps服务器的带宽通常是共享的,实际上手测试出来的带宽往往低于标注的带宽,所以实际上还需要上手实测。

关于网络优化,录播服务器一般不用考虑对大陆是否有优化,主要因为优化线路成本相差比较大。下载录播视频到本地做切片时,如果线路真的很差(很难连接上或者跑不动),可以考虑用优化线路的机器拉,或换本地网络/vps线路再测试。对线路有要求的建议购买前在Bing或者Google上搜索相关测评再考虑是否入手。当然一般vps服务商也有提供退款政策,可提前了解。

1.3、核心和运行内存

只做录播服务器的话,不是很重要,根据前两点选择就好,一般都是溢出的。当然建议最低配置为1H1G

1.4、商家

目前选择的是 CCS 家的 OUT OF STOCK SPECIALS - Holiday Special 4GB RAM,在网站https://colocross.ing/上筛选的,发贴时貌似已无货。目前选择的vps配置大致为 $20.99 USD / 3 vCPU / 4GB RAM / 60GB SSD / 40TB @ 1Gbps。测试同时录制2-3个直播流基本上没什么问题,偶尔可能网络不是很稳定。
商家和vps信息可在各种vps测评网站和论坛找到。由于本人没怎么使用过其它vps进行录播,所以不在此做推荐。

1.5、系统选择

个人习惯使用 Debian 系列,属于比较大众的服务器系统里面资源占用较少的。目前出的 Debian 13 即可,不必过多纠结。

1.6、录播项目的选择

  • BILIUP:主要用来录制 B站 的主播,带自动投稿。但在测试录制 Twitch 平台时经常出现低清晰度和多分段。可录弹幕,不过弹幕需要自己找工具转换和发送。
  • StreamCap:主要用来录制 Twitch 的主播。缺点是本身不带投稿模块,且存在开播延迟录制(约两三分钟),不带弹幕

二、环境配置与部署

由于我第一次部署时,先部署 BILIUP 后部署 StreamCap ,没出现什么异常。但后面重装系统后无论怎么部署都会出现 StreamCap 不会自动检测开播的BUG,可能是两个项目的环境或依赖存在冲突吧。后面改用docker部署了,暂时没发现什么问题。主要步骤如下:

2.1、设置DNS为1.1.1.1 (可选)

修改DNS文件

echo -e "nameserver 1.1.1.1\nnameserver 8.8.8.8" > /etc/resolv.conf

锁定该文件

chattr +i /etc/resolv.conf

2.2、设置时区 (可选)

设置时区为上海

timedatectl set-timezone Asia/Shanghai

验证

date

2.3、开启 BBR 加速 (建议)

BBR 是 Google 开发的拥塞控制算法,对于 VPS 跨海访问的情况,能显著提升高丢包、高延迟环境下的速度。
修改内核参数

echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf

使配置生效

sysctl -p

验证开启状态

sysctl net.ipv4.tcp_congestion_control

2.4、更新系统包 (建议)

apt update && apt upgrade -y

2.5、更改 SSH 默认端口 (建议)

编辑 /etc/ssh/sshd_config

nano /etc/ssh/sshd_config

移动光标到行 #Port 22
手动去掉 # 后把端口改成 10000-60000 之间的高位端口,Ctrl+S保存,Ctrl+X退出

重启ssh

systemctl restart ssh

2.6、安装基础依赖 (建议)

apt install curl wget git tar htop -y

三、Docker 方式部署

3.1、安装 Docker

curl -fsSL https://get.docker.com | bash

验证版本

docker --version
docker compose version

3.2、创建 docker 总目录 (建议,方便管理)

创建 /opt/docker-apps 目录

mkdir -p /opt/docker-apps

设置权限

chown -R $USER:$USER /opt/docker-apps

3.3、编辑 docker-compose.yml (示例为 /opt/docker-apps 目录下)

进入 /opt/docker-apps 目录,编辑该目录下的 docker-compose.yml 文件

cd /opt/docker-apps
nano docker-compose.yml

粘贴如下内容后Ctrl+S保存,Ctrl+X退出

services:
  biliup:
    image: ghcr.io/biliup/caution:latest
    container_name: biliup
    restart: unless-stopped
    volumes:
      - ./biliup:/opt
      - ./biliup/backup:/opt/backup
      - ./streamcap/downloads:/opt/downloads
    ports:
      - "19159:19159"
    environment:
      - TZ=Asia/Shanghai

  streamcap:
    image: ihmily/streamcap
    container_name: streamcap
    restart: unless-stopped
    tty: true
    ports:
      - "6006:6006"
    environment:
      - PLATFORM=web
      - HOST=0.0.0.0
      - PORT=6006
      - TZ=Asia/Shanghai
    volumes:
      - ./streamcap/.env:/app/.env
      - ./streamcap/config:/app/config
      - ./streamcap/downloads:/app/downloads
      - ./streamcap/logs:/app/logs
      - ./streamcap/locales:/app/locales
      - ./streamcap/assets:/app/assets
      - ./streamcap/upload.sh:/app/upload.sh    # 自动投稿脚本
      - /var/run/docker.sock:/var/run/docker.sock       # 用于测试 docker 命令
      - /usr/bin/docker:/usr/bin/docker   # 挂载宿主机 docker 二进制
    healthcheck:
      test: ["CMD", "sh", "-c", "curl -f http://localhost:6006/about"]
      interval: 30s
      timeout: 10s
      retries: 3


  video_api:
    image: ihmily/streamcap
    container_name: video_api
    restart: unless-stopped
    command: python -m app.api.video_stream_service
    ports:
      - "6007:6007"
    environment:
      - TZ=Asia/Shanghai
      - CUSTOM_VIDEO_ROOT_DIR=/app/downloads
    volumes:
      - ./streamcap/downloads:/app/downloads
      - ./streamcap/.env:/app/.env
    depends_on:
      - streamcap

  miniserve:                              # 新增的http服务端,用于在网页端快速下载录播/弹幕文件
    image: svenstaro/miniserve
    container_name: miniserve
    restart: unless-stopped
    command: -z -r /data                  # -z 启用压缩 -r 启用目录浏览
    ports:
      - "8080:8080"                       # 映射到宿主机 8080 端口,可按需修改
    volumes:
      - ./biliup/backup:/data                    # 暴露 biliup/backup 备份目录(也可换成 ./streamcap/downloads)
    environment:
      - TZ=Asia/Shanghai

networks:
  default:
    driver: bridge

3.4、创建必须的文件

定位到 /opt/docker-apps 目录

cd /opt/docker-apps

创建必须的空文件夹(防止报错)

mkdir -p biliup streamcap

克隆StreamCap完整源码

git clone https://github.com/ihmily/StreamCap.git streamcap

复制配置环境文件

cd streamcap
cp .env.example .env

编辑配置环境文件,手动修改平台为网页端、后台对外暴露后Ctrl+S保存,Ctrl+X退出
PLATFORM=web
HOST=0.0.0.0

nano .env

3.5、执行 docker 部署

# 定位到 /opt/docker-apps 目录
cd /opt/docker-apps

# 关闭并删除当前目录下所有容器和临时数据
docker compose down -v

# 后台重新启动所有服务(重新创建容器:本地无镜像则拉,有就用本地)
docker compose up -d

Biliup 面板:http:// 你的 IP:19159
StreamCap 面板:http:// 你的 IP:6006
miniserve 面板:http:// 你的 IP:8080

四、常用命令

4.1、检查是否全部运行

docker ps

4.2、拉取最新镜像

docker compose pull

4.3、重启服务(用新镜像)

docker compose up -d

4.4、只拉 StreamCap 镜像

docker compose pull streamcap

4.5、只重启 StreamCap 和依赖它的 video_api

docker compose up -d streamcap

4.6、清理掉不再用的旧镜像

docker image prune -f

4.7、(已弃用)在 biliup 目录下创建 streamcap/downloads/Twitch 的快捷方式,以便于快速跳转 (示例主播:飯絲児ファンシ)

ln -s /opt/docker-apps/streamcap/downloads/Twitch/飯絲児ファンシ /opt/docker-apps/biliup/飯絲児ファンシ

4.8、(已弃用)一键移动 streamcap 的 Twitch 主播录播文件到 biliup 目录下,以便于使用 biliup 网页端投稿和预览 (示例主播:飯絲児ファンシ)

mv /opt/docker-apps/streamcap/downloads/Twitch/飯絲児ファンシ/* /opt/docker-apps/biliup/

4.9、(已弃用)一键删除 biliup 目录下指定的主播录播文件,以腾出空间 (示例主播:飯絲児ファンシ),删除前注意是否已经投稿/下载到本地

rm -f /opt/docker-apps/biliup/飯絲児ファンシ*.mp4

五、常用 Biliup 投稿模板

5.1、模板名

根据实际情况自己修改

主播名【直播回放】

5.2、投稿标题

自动投稿,无需修改

【%Y-%m-%d】【{streamer}】{title}

手动投稿,除了日期会取当天,其它的不会自动识别,需要根据情况自己修改

【%Y-%m-%d】【主播名】标题

5.3、转载说明

根据情况自己修改

飯絲児 Twitch 直播间:https://www.twitch.tv/fancy_ito

5.4、标签

根据情况自己修改

直播回放,飯絲児ファンシ,fancy_ito,VTuber,虚拟主播,゙悅音幻絲

5.5、简介

根据情况自己修改

【゙悅音幻絲】
个人空间:https://space.bilibili.com/19366645
直播间:https://live.bilibili.com/473323
平时回放在【饭丝儿IV岁啦w_录播姬】
https://space.bilibili.com/1186069249

关注饭丝喵谢谢喵~

六、StreamCap 自动投稿的配置

6.1、在streamcap目录下新建 upload.sh 文件

跳转到/opt/docker-apps/streamcap目录并编辑upload.sh

cd /opt/docker-apps/streamcap
nano upload.sh

粘贴如下内容后Ctrl+S保存,Ctrl+X退出

#!/bin/bash

# ============================================
# biliup 自动投稿脚本(完全配置化版)
# 所有参数均可由主播配置文件覆盖,未配置则自动生成。
# 新增:日志轮转(超过2MB自动切分,保留3个备份)
# ============================================

# --- 0. 初始化与日志设置 ---
STREAMER="$1"
STREAMER="${STREAMER#\"}"
STREAMER="${STREAMER%\"}"
STREAMER="${STREAMER#\'}"
STREAMER="${STREAMER%\'}"

PATH_TEMPLATE="$2"
PATH_TEMPLATE="${PATH_TEMPLATE#\"}"
PATH_TEMPLATE="${PATH_TEMPLATE%\"}"
PATH_TEMPLATE="${PATH_TEMPLATE#\'}"
PATH_TEMPLATE="${PATH_TEMPLATE%\'}"

FORMAT="$3"
SPLIT_FLAG="$4"
CONVERT_FLAG="$5"

LOG_FILE="/app/logs/upload.log"
CONFIG_DIR="/app/config"
LOCK_DIR="/tmp/streamcap_locks"
mkdir -p "$LOCK_DIR"

# --- 日志轮转函数 ---
rotate_log() {
    local log_file="$1"
    local max_size=$((2 * 1024 * 1024))  # 2MB
    local keep_count=3

    if [ ! -f "$log_file" ]; then
        return
    fi

    # 获取文件大小(兼容 Linux 和 macOS)
    local size
    size=$(stat -c%s "$log_file" 2>/dev/null || stat -f%z "$log_file" 2>/dev/null)
    if [ -z "$size" ] || [ "$size" -le "$max_size" ]; then
        return
    fi

    # 轮转:移动旧日志并创建新文件
    local timestamp
    timestamp=$(date +%Y%m%d_%H%M%S)
    mv "$log_file" "${log_file}.${timestamp}"
    touch "$log_file"
    echo "$(date): 日志文件已轮转(大小超过 $((max_size/1024/1024))MB)" >> "$log_file"

    # 清理旧备份,只保留最近 $keep_count 个
    local backups
    backups=($(ls -t "${log_file}."* 2>/dev/null))
    if [ ${#backups[@]} -gt "$keep_count" ]; then
        local to_delete=("${backups[@]:$keep_count}")
        for f in "${to_delete[@]}"; do
            rm -f "$f"
        done
    fi
}

# 在首次写日志前执行轮转检查
rotate_log "$LOG_FILE"

# 定义日志函数(在轮转之后)
log() {
    echo "$(date): $*" >> "$LOG_FILE"
}

log "脚本被触发,主播: $STREAMER"

# 检查 docker 命令
if ! command -v docker &> /dev/null; then
    log "docker 命令不可用,尝试安装 docker-cli ..."
    if command -v apk &> /dev/null; then
        apk add --no-cache docker-cli >> "$LOG_FILE" 2>&1
        if ! command -v docker &> /dev/null; then
            log "安装 docker-cli 失败,请手动安装。"
            exit 1
        fi
    else
        log "非 Alpine 系统,无法自动安装 docker-cli,请手动安装。"
        exit 1
    fi
fi

# --- 1. 解析真实文件路径 ---
resolve_real_path() {
    local template_path="$1"
    template_path="${template_path#\"}"
    template_path="${template_path%\"}"
    
    if [ -f "$template_path" ]; then
        echo "$template_path"
        return 0
    fi
    
    local pattern=$(echo "$template_path" | sed 's/%0[0-9]d/*/g')
    if [[ "$pattern" != "$template_path" ]]; then
        shopt -s nullglob
        local matched_files=($pattern)
        shopt -u nullglob
        if [ ${#matched_files[@]} -gt 0 ]; then
            local sorted_file=$(printf '%s\n' "${matched_files[@]}" | sort | head -n1)
            echo "$sorted_file"
            return 0
        fi
    fi
    return 1
}

REAL_FILE_PATH=$(resolve_real_path "$PATH_TEMPLATE")
if [ -z "$REAL_FILE_PATH" ]; then
    log "错误 - 无法获取有效的录制文件路径,脚本退出。传入值: '$PATH_TEMPLATE'"
    exit 1
fi

log "处理文件: $REAL_FILE_PATH"

STREAMER_DIR=$(dirname "$REAL_FILE_PATH")
BASE_PREFIX=$(basename "$REAL_FILE_PATH" | sed 's/_[0-9][0-9][0-9]\.mp4$//' | sed 's/_%03d$//')
log "主播目录: $STREAMER_DIR, 文件前缀: $BASE_PREFIX"

# --- 提取直播日期 ---
LIVE_DATE=$(echo "$BASE_PREFIX" | grep -oP '\d{4}-\d{2}-\d{2}' | head -1)
if [ -z "$LIVE_DATE" ]; then
    LIVE_DATE=$(date +%Y-%m-%d)
    log "警告: 无法从文件名提取日期,使用当前日期 $LIVE_DATE"
else
    log "提取直播日期: $LIVE_DATE"
fi

# --- 2. 会话锁机制 ---
get_session_id() {
    local first_file_path="$1"
    local dir_name=$(dirname "$first_file_path")
    local first_file_name=$(basename "$first_file_path")
    local session_start_time=$(echo "$first_file_name" | grep -oP '\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}' | head -1)
    
    if [ -n "$session_start_time" ]; then
        session_start_time=$(echo "$session_start_time" | sed 's/[-_]//g')
    else
        session_start_time=$(date +%Y%m%d)
    fi
    
    echo "${dir_name}_${session_start_time}_session" | sed 's|/|_|g'
}

SESSION_ID=$(get_session_id "$REAL_FILE_PATH")
LOCK_FILE="${LOCK_DIR}/${SESSION_ID}.lock"

if [ -f "$LOCK_FILE" ]; then
    LOCK_MTIME=$(stat -c %Y "$LOCK_FILE" 2>/dev/null)
    NOW=$(date +%s)
    if [ -n "$LOCK_MTIME" ] && [ $((NOW - LOCK_MTIME)) -gt 1800 ]; then
        log "发现过期锁文件(超过30分钟),强制删除: $LOCK_FILE"
        rm -f "$LOCK_FILE"
    fi
fi

exec 200>"$LOCK_FILE"
if ! flock -n 200; then
    log "已有投稿任务在执行(锁文件: $LOCK_FILE),本脚本退出。"
    exit 0
fi
log "获取锁成功: $LOCK_FILE"

cleanup() {
    flock -u 200 2>/dev/null
    rm -f "$LOCK_FILE"
    log "已释放锁并清理锁文件"
}
trap cleanup EXIT

# --- 3. 等待写入完成 ---
sleep 60

# --- 4. 收集同前缀的所有分段 ---
ALL_SEGMENTS=$(find "$STREAMER_DIR" -maxdepth 1 -name "${BASE_PREFIX}_*.mp4" 2>/dev/null | sort)
if [ -z "$ALL_SEGMENTS" ]; then
    log "未找到任何可投稿的视频文件,退出。"
    exit 1
fi

SEGMENT_FILES=$(echo "$ALL_SEGMENTS" | sort -u)
SEGMENT_COUNT=$(echo "$SEGMENT_FILES" | wc -l)
log "共找到 $SEGMENT_COUNT 个分段文件"

convert_path() { echo "$1" | sed 's|/app/downloads|/opt/downloads|'; }

# --- 5. 从日志提取直播信息(支持日志轮转,搜索 streamget*) ---
get_live_info_from_log() {
    local streamer="$1"
    local log_files=()
    
    # 收集所有 streamget 开头的日志文件(包括轮转文件),按修改时间从新到旧排序
    while IFS= read -r f; do
        log_files+=("$f")
    done < <(find /app/logs -maxdepth 1 -name "streamget*" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | cut -d' ' -f2-)
    
    # 如果没找到任何 streamget 文件,再尝试查找其他 .log 文件(兼容)
    if [ ${#log_files[@]} -eq 0 ]; then
        while IFS= read -r f; do
            log_files+=("$f")
        done < <(find /app/logs -name "*.log" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | cut -d' ' -f2-)
    fi
    
    if [ ${#log_files[@]} -eq 0 ]; then
        log "未找到任何 StreamCap 日志文件"
        return 1
    fi
    
    # 转义主播名中的特殊字符
    local escaped_streamer=$(echo "$streamer" | sed 's/[][\.*^$()+?{}|]/\\&/g')
    
    # 遍历日志文件(从新到旧)
    for lf in "${log_files[@]}"; do
        # 从最新行开始读取,查找包含主播名且含有 title= 的记录
        local raw_line=$(tac "$lf" 2>/dev/null | grep -m1 "anchor_name=['\"]$escaped_streamer" | grep "title=")
        if [ -n "$raw_line" ]; then
            local title=""; local live_url=""
            if [[ "$raw_line" =~ title=\'([^\']+)\' ]]; then
                title="${BASH_REMATCH[1]}"
            elif [[ "$raw_line" =~ title=\"([^\"]+)\" ]]; then
                title="${BASH_REMATCH[1]}"
            fi
            if [[ "$raw_line" =~ live_url=\'([^\']+)\' ]]; then
                live_url="${BASH_REMATCH[1]}"
            elif [[ "$raw_line" =~ live_url=\"([^\"]+)\" ]]; then
                live_url="${BASH_REMATCH[1]}"
            fi
            if [ -n "$title" ] && [ -n "$live_url" ]; then
                log "从日志文件 $lf 提取成功: title=$title, live_url=$live_url"
                echo "${title}||${live_url}"
                return 0
            fi
        fi
    done
    
    log "在所有日志文件中均未找到包含主播名 '$streamer' 的记录"
    return 1
}

LIVE_INFO=$(get_live_info_from_log "$STREAMER")
if [ -n "$LIVE_INFO" ]; then
    LIVE_TITLE="${LIVE_INFO%%||*}"
    LIVE_URL="${LIVE_INFO##*||}"
else
    LIVE_TITLE=""
    LIVE_URL=""
fi

# --- 6. 加载主播配置(可覆盖标题、描述、来源等) ---
# 默认值
DEFAULT_TID="21"
DEFAULT_TAG="直播回放,Twitch,${STREAMER//,/、}"
DEFAULT_COPYRIGHT="2"
DEFAULT_LINE="txa"
DEFAULT_SOURCE_FORMAT="%s Twitch 直播间:%s"

# 先设置默认值
TID="$DEFAULT_TID"
TAG="$DEFAULT_TAG"
COPYRIGHT="$DEFAULT_COPYRIGHT"
LINE="$DEFAULT_LINE"
SOURCE_FORMAT="$DEFAULT_SOURCE_FORMAT"
# 配置文件可能定义的覆盖变量
TITLE=""
DESCRIPTION=""
SOURCE_INFO=""

# 加载配置文件(如果存在)
conf_file="${CONFIG_DIR}/${STREAMER}.conf"
if [ -f "$conf_file" ]; then
    # shellcheck source=/dev/null
    source "$conf_file"
    log "加载主播 $STREAMER 专属配置"
fi

# --- 7. 自动生成未配置的参数 ---
# 标题:如果配置文件未设置 TITLE,则自动生成
if [ -z "$TITLE" ]; then
    if [ -n "$LIVE_TITLE" ]; then
        TITLE="【${LIVE_DATE}】【$STREAMER】$LIVE_TITLE"
    else
        TITLE="【${LIVE_DATE}】【$STREAMER】直播回放"
    fi
fi

# --- 标题长度截断(B站限制80字符) ---
MAX_TITLE_LEN=80
if [ ${#TITLE} -gt $MAX_TITLE_LEN ]; then
    # 保留固定部分 "【日期】【主播】",截断直播标题部分
    FIXED_PART="【${LIVE_DATE}】【${STREAMER}】"
    FIXED_LEN=${#FIXED_PART}
    # 计算直播标题允许的最大长度(留出 "..." 的3个字符)
    MAX_LIVE_TITLE_LEN=$((MAX_TITLE_LEN - FIXED_LEN - 3))
    if [ $MAX_LIVE_TITLE_LEN -gt 0 ] && [ -n "$LIVE_TITLE" ]; then
        # 如果原始标题中有直播标题,截断它
        ORIGINAL_LIVE_TITLE="$LIVE_TITLE"
        if [ ${#ORIGINAL_LIVE_TITLE} -gt $MAX_LIVE_TITLE_LEN ]; then
            LIVE_TITLE="${ORIGINAL_LIVE_TITLE:0:$MAX_LIVE_TITLE_LEN}..."
        fi
        # 重新构建标题
        TITLE="${FIXED_PART}${LIVE_TITLE}"
    else
        # 如果无法区分固定部分,则直接截断整个标题
        TITLE="${TITLE:0:$MAX_TITLE_LEN}"
    fi
    log "警告:标题超过80字符,已截断至 ${#TITLE} 字符"
fi

# 描述:如果未设置 DESCRIPTION,则自动使用直播标题
if [ -z "$DESCRIPTION" ]; then
    DESCRIPTION="$LIVE_TITLE"
fi

# 来源信息:如果未设置 SOURCE_INFO,则根据 SOURCE_FORMAT 生成
if [ -z "$SOURCE_INFO" ]; then
    if [ -n "$LIVE_URL" ]; then
        SOURCE_INFO=$(printf "$SOURCE_FORMAT" "$STREAMER" "$LIVE_URL")
    else
        SOURCE_INFO=$(printf "$SOURCE_FORMAT" "$STREAMER" "https://www.twitch.tv/${STREAMER}")
    fi
fi

log "最终标题: $TITLE"
log "最终描述: $DESCRIPTION"
log "最终来源: $SOURCE_INFO"

# --- 8. 执行投稿 ---
cmd=(docker exec -w /opt biliup biliup upload)
while IFS= read -r line; do cmd+=("$(convert_path "$line")"); done <<< "$SEGMENT_FILES"
cmd+=(--title "$TITLE" --tid "$TID" --tag "$TAG" --copyright "$COPYRIGHT" --line "$LINE" --source "$SOURCE_INFO" --desc "$DESCRIPTION")
log "执行投稿: ${cmd[*]}"
"${cmd[@]}" >> "$LOG_FILE" 2>&1
UPLOAD_EXIT=$?

# --- 9. 清理 ---
if [ $UPLOAD_EXIT -eq 0 ]; then
    log "投稿成功,清理文件。"
    while IFS= read -r line; do rm -f "$line"; done <<< "$SEGMENT_FILES"
else
    log "投稿失败,保留文件待处理。"
fi

exit $UPLOAD_EXIT

6.2、StreamCap 网页端设置

打开StreamCap网页端,切换到设置,在录制设置项目下的自定义脚本执行命令处输入/app/upload.sh
切换到录制,检查主播名是否为主播对外显示的昵称,默认添加的可能带直播间ID,如稻乙緹-iitifox,应改成稻乙緹

6.3、将宿主机的 Docker 二进制文件挂载到容器内,实现Docker项目中调用Docker命令

执行which docker确认宿主机 Docker 路径为/usr/bin/docker
前面已在docker-compose.yml文件中streamcap服务的volumes中添加挂载- /usr/bin/docker:/usr/bin/docker # 挂载宿主机 docker 二进制

6.4、重启docker

重启后记得手动打开一次StreamCap网页端,不然StreamCap可能不会开始工作。

docker compose down
docker compose up -d

6.5、验证docker命令是否可用

如果显示版本信息,则挂载成功。

docker exec -it streamcap docker --version

6.6、upload.sh调用的主播投稿模板

/opt/docker-apps/streamcap/config目录下新建主播名.conf
如(以主播飯絲児ファンシ为例)
切换到该配置目录

cd /opt/docker-apps/streamcap/config

编辑主播飯絲児ファンシ配置文件,主播名根据需要自己修改

nano 飯絲児ファンシ.conf

粘贴预先写好的内容,根据需要自己修改(一般只需要设置标签和简介,标题自动生成,来源如果生成的有问题也可以在配置文件固定写好,去掉前面的#注释符)
然后Ctrl+S保存,Ctrl+X退出

# 标题(若不写则自动生成)
# TITLE="【2026-06-21】【飯絲児ファンシ】直播录播"

# 分区
# TID="21"

# 标签
TAG="直播回放,飯絲児ファンシ,悅音幻絲,fancy_ito"

# 来源信息
# SOURCE_INFO="飯絲児ファンシ Twitch 直播间:https://www.twitch.tv/fancy_ito"

# 覆盖描述
DESCRIPTION="【゙悅音幻絲】
个人空间:https://space.bilibili.com/19366645
直播间:https://live.bilibili.com/473323
平时回放在【饭丝儿IV岁啦w_录播姬】
https://space.bilibili.com/1186069249

关注饭丝喵谢谢喵~"

# 线路
# LINE="txa"

6.7、Biliup命令行登录

切换到/opt/docker-apps/目录,执行登录,未登录会找不到cookie文件导致投稿失败

cd /opt/docker-apps
docker exec -it biliup biliup login