持久化 HLS 会话
本项目通过 JNI 封装 FFmpeg 库,实现了一个持久化 HLS 会话模块。该模块支持如下功能:
初始化持久化 HLS 会话
创建 HLS 输出上下文,并保存参数选项,同时记录会话的创建时间。该接口返回一个 native 指针,用于后续追加视频分段或关闭会话。追加视频分段
根据输入的 MP4 文件,追加视频(和音频)分段到持久化 HLS 会话中。首次追加时会基于输入流自动创建输出流并写入播放列表头信息(延迟写 header 的方式确保输出流参数来自真实输入),随后每个追加将更新时间戳偏移,确保连续性。结束持久化 HLS 会话
调用 finishPersistentHls 时写入 trailer(例如 EXT‑X‑ENDLIST 标签),关闭并释放输出上下文以及所有分配的资源,同时从全局会话列表中删除该会话。列出与释放会话
提供 listHlsSession 用于向 Java 层返回 JSON 格式的活跃会话列表;同时提供 freeHlsSession 接口允许直接释放会话而不写 trailer(适用于需要直接清理资源的场景)。定时任务
另外在 Java 层实现了 HlsSessionCleaner 定时任务,每小时(或测试时每 10 秒)运行一次,根据返回的会话信息自动检查是否需要释放会话,避免资源长时间占用。
下面分别给出 Java 层代码和 C 层代码的完整实现。
一、Java 代码
1.1 NativeMedia 类
该类定义了所有对外公开的 native 方法接口。部分方法用于音频处理(如 splitMp3, mp4ToMp3 等),这里重点介绍与 HLS 会话相关的接口。请注意类加载时调用了 LibraryUtils.load() 和 HlsSessionCleaner.start(),确保动态库加载和定时任务启动。
package com.litongjava.media;
import com.litongjava.media.task.HlsSessionCleaner;
import com.litongjava.media.utils.LibraryUtils;
public class NativeMedia {
static {
LibraryUtils.load();
HlsSessionCleaner.start();
}
/**
* Initializes and loads the library
*/
public static void init() {
}
/**
* Splits an MP3 file into smaller MP3 files of specified size.
*
* @param srcPath Path to the source MP3 file
* @param size Maximum size in bytes for each output file
* @return Array of paths to the generated MP3 files
*/
public static native String[] splitMp3(String srcPath, long size);
/**
* Converts an MP4 video file to an MP3 audio file.
*
* @param inputPath Path to the input MP4 file
* @return Path to the output MP3 file on success, or an error message on failure
*/
public static native String mp4ToMp3(String inputPath);
public static native String toMp3(String inputPath);
public static native String convertTo(String inputPath, String targetFormat);
public static native String[] split(String srcPath, long size);
public static native String[] supportFormats();
/**
* Native method to be implemented by C.
*
* The implementation should include:
* 1. Using the native‑media library to convert the MP4 file specified by inputMp4Path into HLS segments,
* with each segment lasting segmentDuration seconds, and the starting segment number determined by sceneIndex.
* 2. Naming the generated TS segment files according to tsPattern, and storing them in the same directory
* as the playlistUrl file.
* 3. Generating an m3u8 segment based on the converted TS segment information (including the #EXTINF tag
* and the TS file name for each segment).
* 4. Appending the generated m3u8 segment content to the playlist file specified by playlistUrl (ensure that
* the playlist does not contain the #EXT-X-ENDLIST tag, otherwise the append will be ineffective).
*
* @param hlsPath Full path to the playlist file
* @param inputMp4Path Path to the input MP4 file
* @param tsPattern Naming template for TS segment files (including the directory path)
* @param sceneIndex Current scene index (starting segment number for conversion)
* @param segmentDuration Segment duration in seconds
*/
public static native String splitVideoToHLS(String hlsPath, String inputMp4Path, String tsPattern, int segmentDuration);
/**
* 初始化持久化 HLS 会话,返回一个表示会话的 native 指针
* @param playlistUrl HLS 播放列表保存路径,如 "./data/hls/test/playlist.m3u8"
* @param tsPattern TS 分段文件命名模板,如 "./data/hls/test/segment_%03d.ts"
* @param startNumber 起始分段编号
* @param segmentDuration 分段时长(秒)
* @return 会话指针(long 类型),后续操作需要传入该指针
*/
public static native long initPersistentHls(String playlistUrl, String tsPattern, int startNumber, int segmentDuration);
/**
* 追加一个 MP4 分段到指定的 HLS 会话中
* @param sessionPtr 会话指针(由 initPersistentHls 返回)
* @param inputMp4Path 输入 MP4 文件路径
* @return 状态信息
*/
public static native String appendVideoSegmentToHls(long sessionPtr, String inputMp4Path);
/**
* 在当前音频 HLS 会话中插入一个静音段
* 静音段的时长由 duration 指定,单位为秒。
*
* @param sessionPtr 会话指针(由 initPersistentHls 返回)
* @param duration 静音段时长(秒)
* @return 状态信息
*/
public static native String insertSilentSegment(long sessionPtr, double duration);
/**
* 结束指定的 HLS 会话,写入 EXT‑X‑ENDLIST 并关闭输出,同时释放会话资源
* @param sessionPtr 会话指针(由 initPersistentHls 返回)
* @param playlistUrl 播放列表路径
* @return 状态信息
*/
public static native String finishPersistentHls(long sessionPtr, String playlistUrl);
/**
* Merges multiple video/audio files into a single output file using stream copy.
* This method calls a native C function that utilizes the FFmpeg command-line tool.
* The input files should ideally have compatible stream parameters (codec, resolution, etc.)
* for stream copy to work reliably and efficiently.
*
* @param inputPaths An array of absolute paths to the input media files.
* @param outputPath The absolute path for the merged output media file.
* @return true if the merging process initiated by FFmpeg completes successfully (exit code 0), false otherwise.
* @throws NullPointerException if inputPaths or outputPath is null, or if inputPaths contains null elements.
* @throws IllegalArgumentException if inputPaths contains fewer than 2 files.
*/
public static native boolean merge(String[] inputPaths, String outputPath);
/**
* 列出当前所有活跃的 HLS 会话及相关信息,如会话创建时间、当前时间偏移等
* @return JSON 格式的字符串列表,每个对象描述一个会话的信息
*/
public static native String listHlsSession();
/**
* 释放指定的 HLS 会话资源,不生成播放列表 trailer(用于直接释放会话)。
* @param sessionPtr 会话指针(由 initPersistentHls 返回)
* @return 状态信息
*/
public static native String freeHlsSession(long sessionPtr);
}
1.2 HlsSessionCleaner 定时任务
该定时任务每隔一定时间运行一次,调用 NativeMedia.listHlsSession 获取当前活跃会话,通过硬编码的正则表达式提取每个会话的 sessionPtr 与 createdTime,并判断如果某个会话的创建时间已经超过指定时间,则调用 NativeMedia.freeHlsSession 释放会话资源。
package com.litongjava.media.task;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.litongjava.media.NativeMedia;
/**
* HlsSessionCleaner 定时任务类
*
* 每隔 1 小时运行一次:
* 1. 调用 NativeMedia.listHlsSession 获取当前所有活跃的 HLS 会话,返回 JSON 字符串;
* 2. 使用硬编码提取方式(正则表达式)解析每个会话的 sessionPtr 和 createdTime;
* 3. 如果某个会话的创建时间距离当前时间超过 1 小时,则调用 NativeMedia.freeHlsSession 关闭该会话。
*/
public class HlsSessionCleaner {
// 定义日期格式,与 native 层返回的 createdTime 格式一致
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 定时任务采用 ScheduledExecutorService
private static ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
static {
// 安排任务:初始延迟 0,之后每小时执行一次
Runnable task = new Runnable() {
@Override
public void run() {
try {
// 调用 NativeMedia.listHlsSession 获取会话列表,返回 JSON 格式字符串
String sessionListJson = NativeMedia.listHlsSession();
System.out.println("Current active sessions: " + sessionListJson);
// 如果返回值为 "[]" 或者为空,则直接返回
if (sessionListJson == null || sessionListJson.trim().equals("[]")) {
System.out.println("No active sessions.");
return;
}
// 去掉前后中括号
sessionListJson = sessionListJson.trim();
if (sessionListJson.startsWith("[")) {
sessionListJson = sessionListJson.substring(1);
}
if (sessionListJson.endsWith("]")) {
sessionListJson = sessionListJson.substring(0, sessionListJson.length() - 1);
}
// 按 "}," 分割字符串(这里假设每个会话对象都以 '}' 结尾,且对象之间以 "}," 分割)
String[] sessions = sessionListJson.split("\\},");
for (String sessionJson : sessions) {
sessionJson = sessionJson.trim();
// 如果没有闭合右括号,则补上
if (!sessionJson.endsWith("}")) {
sessionJson = sessionJson + "}";
}
// 提取 sessionPtr
Pattern ptrPattern = Pattern.compile("\"sessionPtr\"\\s*:\\s*(-?\\d+)");
Matcher ptrMatcher = ptrPattern.matcher(sessionJson);
long sessionPtr = 0;
if (ptrMatcher.find()) {
sessionPtr = Long.parseLong(ptrMatcher.group(1));
}
// 如果没提取到有效的 sessionPtr,则跳过此项
if (sessionPtr == 0) {
System.out.println("Skipped a session because sessionPtr is 0.");
continue;
}
// 提取 createdTime
Pattern timePattern = Pattern.compile("\"createdTime\"\\s*:\\s*\"([^\"]+)\"");
Matcher timeMatcher = timePattern.matcher(sessionJson);
String createdTimeStr = "";
if (timeMatcher.find()) {
createdTimeStr = timeMatcher.group(1);
}
// 如果 createdTime 字段为空,则跳过该会话
if (createdTimeStr.isEmpty()) {
System.out.println("Skipped a session because createdTime is empty for sessionPtr: " + sessionPtr);
continue;
}
// 将 createdTime 字符串转换为 Date 对象
Date createdDate = DATE_FORMAT.parse(createdTimeStr);
long createdMillis = createdDate.getTime();
long nowMillis = System.currentTimeMillis();
// 判断是否超过 1 小时(3600000 毫秒),正式环境使用 3600000L,测试时可调短时间
if (nowMillis - createdMillis > 3600000L) {
// 调用 NativeMedia.freeHlsSession 释放会话资源
String result = NativeMedia.freeHlsSession(sessionPtr);
System.out.println("Closed HLS session " + sessionPtr + ", result: " + result);
}
}
} catch (ParseException e) {
System.err.println("Failed to parse createdTime: " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
System.err.println("Exception in HLS session cleaner: " + e.getMessage());
e.printStackTrace();
}
}
};
scheduler.scheduleAtFixedRate(task, 0, 1, TimeUnit.HOURS);
// 如测试时可使用下面这一行:
// scheduler.scheduleAtFixedRate(task, 0, 10, TimeUnit.SECONDS);
}
public static void start() {
// 保持类加载即可,任务已在 static 块中启动
}
}
1.3 HlsSenceTest 测试代码
该测试代码用于验证持久化 HLS 会话的初始化、视频分段追加和结束会话功能,并打印会话信息。
package com.litongjava.linux.service;
import java.io.File;
import org.junit.Test;
import com.litongjava.media.NativeMedia;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HlsSenceTest {
@Test
public void testSession() {
// 配置测试用路径,请根据实际情况修改
String playlistUrl = "./data/hls/test/main.m3u8";
String tsPattern = "./data/hls/test/segment_video_%03d.ts";
int startNumber = 0;
int segmentDuration = 10; // 每个分段时长(秒)
// 初始化持久化 HLS 会话,返回 session 指针
String listHlsSession = NativeMedia.listHlsSession();
log.info("session:{}", listHlsSession);
long sessionPtr = NativeMedia.initPersistentHls(playlistUrl, tsPattern, startNumber, segmentDuration);
System.out.println("Session pointer: " + sessionPtr);
String folderPath = "C:\\Users\\Administrator\\Downloads";
File folderFile = new File(folderPath);
File[] listFiles = folderFile.listFiles();
for (int i = 0; i < listFiles.length; i++) {
log.info("filename:{}", listFiles[i].getName());
if (listFiles[i].getName().endsWith(".mp4")) {
System.out.println(NativeMedia.appendVideoSegmentToHls(sessionPtr, listFiles[i].getAbsolutePath()));
listHlsSession = NativeMedia.listHlsSession();
log.info("session:{}", listHlsSession);
}
}
// 结束会话
listHlsSession = NativeMedia.listHlsSession();
log.info("session:{}", listHlsSession);
System.out.println(NativeMedia.finishPersistentHls(sessionPtr, playlistUrl));
listHlsSession = NativeMedia.listHlsSession();
log.info("session:{}", listHlsSession);
}
}
二、C 代码
下面给出完整的 C 代码实现。代码中包含了所有 HLS 会话相关接口的实现:
- 初始化会话(initPersistentHls)
- 追加视频分段(appendVideoSegmentToHls)
- 列出当前会话(listHlsSession)
- 结束会话(finishPersistentHls)
- 直接释放会话(freeHlsSession)
此外,代码中还维护了一个全局链表(g_hlsSessionHead)用于管理所有活跃的会话,并增加了同步查找防止重复释放的辅助函数。
#include "com_litongjava_media_NativeMedia.h"
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#endif
// FFmpeg Headers
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/opt.h>
#include <libavcodec/avcodec.h>
#include <libavutil/channel_layout.h>
#include <libavutil/error.h>
#include <libavutil/mathematics.h>
#include <libavutil/mem.h>
#include <libavutil/samplefmt.h>
#include <libavutil/timestamp.h>
// 内联函数:拷贝 AVChannelLayout(忽略 opaque 字段)
inline int av_channel_layout_copy(AVChannelLayout *dst, const AVChannelLayout *src) {
if (!dst || !src) return AVERROR(EINVAL);
memcpy(dst, src, sizeof(*dst));
dst->opaque = NULL; // 简化处理,不复制 opaque 字段
return 0;
}
typedef struct HlsSession {
AVFormatContext *ofmt_ctx; // HLS muxer 输出上下文
int segDuration; // 分段时长(秒)
int nextSegmentNumber; // 当前下一个分段编号
int64_t global_offset; // 全局时间戳偏移量
char *ts_pattern; // TS 分段文件命名模板(深拷贝保存)
int header_written; // 标识是否已写 header
AVDictionary *opts; // 保存 HLS 配置选项
time_t created_time; // 会话创建时间
int closed; // 0 表示未释放,1 表示已释放
} HlsSession;
typedef struct HlsSessionNode {
HlsSession *session;
struct HlsSessionNode *next;
} HlsSessionNode;
// 全局链表头指针,用于维护所有活跃的 HLS 会话
static HlsSessionNode *g_hlsSessionHead = NULL;
// 添加一个会话到全局链表
static void add_hls_session(HlsSession *session) {
HlsSessionNode *node = (HlsSessionNode *) malloc(sizeof(HlsSessionNode));
if (node) {
node->session = session;
node->next = g_hlsSessionHead;
g_hlsSessionHead = node;
}
}
// 从全局链表中删除一个会话
static void remove_hls_session(HlsSession *session) {
HlsSessionNode **curr = &g_hlsSessionHead;
while (*curr) {
if ((*curr)->session == session) {
HlsSessionNode *tmp = *curr;
*curr = tmp->next;
free(tmp);
return;
}
curr = &((*curr)->next);
}
}
// 辅助函数:查找全局列表中是否存在给定的会话
static HlsSession *find_hls_session(HlsSession *sessionPtrValue) {
HlsSessionNode *curr = g_hlsSessionHead;
while (curr) {
if (curr->session == sessionPtrValue) {
return curr->session;
}
curr = curr->next;
}
return NULL;
}
JNIEXPORT jlong JNICALL Java_com_litongjava_media_NativeMedia_initPersistentHls
(JNIEnv *env, jclass clazz, jstring playlistUrlJ, jstring tsPatternJ, jint startNumber, jint segDuration) {
const char *playlistUrl = (*env)->GetStringUTFChars(env, playlistUrlJ, NULL);
const char *tsPattern = (*env)->GetStringUTFChars(env, tsPatternJ, NULL);
HlsSession *session = (HlsSession *) malloc(sizeof(HlsSession));
if (!session) {
(*env)->ReleaseStringUTFChars(env, playlistUrlJ, playlistUrl);
(*env)->ReleaseStringUTFChars(env, tsPatternJ, tsPattern);
return 0;
}
memset(session, 0, sizeof(HlsSession));
session->segDuration = segDuration;
session->nextSegmentNumber = startNumber;
session->global_offset = 0;
session->header_written = 0; // header 尚未写入
session->ts_pattern = strdup(tsPattern);
if (!session->ts_pattern) {
free(session);
(*env)->ReleaseStringUTFChars(env, playlistUrlJ, playlistUrl);
(*env)->ReleaseStringUTFChars(env, tsPatternJ, tsPattern);
return 0;
}
// 记录会话创建时间
session->created_time = time(NULL);
session->closed = 0;
// 创建输出上下文,指定 muxer 为 "hls",目标为 playlistUrl
AVFormatContext *ofmt_ctx = NULL;
int ret = avformat_alloc_output_context2(&ofmt_ctx, NULL, "hls", playlistUrl);
if (ret < 0 || !ofmt_ctx) {
free(session->ts_pattern);
free(session);
(*env)->ReleaseStringUTFChars(env, playlistUrlJ, playlistUrl);
(*env)->ReleaseStringUTFChars(env, tsPatternJ, tsPattern);
return 0;
}
// 构造 HLS 选项字典,并保存到 session->opts
AVDictionary *opts = NULL;
char seg_time_str[16] = {0};
snprintf(seg_time_str, sizeof(seg_time_str), "%d", segDuration);
av_dict_set(&opts, "hls_time", seg_time_str, 0);
// 使用用户指定的 tsPattern,例如 "segment_video_%03d.ts"
av_dict_set(&opts, "hls_segment_filename", tsPattern, 0);
av_dict_set(&opts, "hls_flags", "append_list", 0);
av_dict_set(&opts, "hls_list_size", "0", 0);
av_dict_set(&opts, "hls_playlist_type", "event", 0);
char start_num_str[16] = {0};
snprintf(start_num_str, sizeof(start_num_str), "%d", startNumber);
av_dict_set(&opts, "start_number", start_num_str, 0);
session->opts = opts;
// 如果输出格式要求打开文件,则调用 avio_open
if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) {
ret = avio_open(&ofmt_ctx->pb, playlistUrl, AVIO_FLAG_WRITE);
if (ret < 0) {
av_dict_free(&opts);
avformat_free_context(ofmt_ctx);
free(session->ts_pattern);
free(session);
(*env)->ReleaseStringUTFChars(env, playlistUrlJ, playlistUrl);
(*env)->ReleaseStringUTFChars(env, tsPatternJ, tsPattern);
return 0;
}
}
session->ofmt_ctx = ofmt_ctx;
(*env)->ReleaseStringUTFChars(env, playlistUrlJ, playlistUrl);
(*env)->ReleaseStringUTFChars(env, tsPatternJ, tsPattern);
// 加入全局会话列表
add_hls_session(session);
return (jlong) session;
}
/*
* Class: com_litongjava_media_NativeMedia
* Method: finishPersistentHls
* Signature: (JLjava/lang/String;)Ljava/lang/String;
*
* 实现说明:
* 1. 根据传入的会话指针,取出 HLS 会话结构体;
* 2. 调用 av_write_trailer 写入 trailer(如生成 EXT‑X‑ENDLIST 标签),关闭输出流;
* 3. 释放输出上下文以及会话中分配的资源(例如 TS 模板字符串和会话结构体);
* 4. 返回结束操作的状态信息。
*/
JNIEXPORT jstring JNICALL Java_com_litongjava_media_NativeMedia_finishPersistentHls
(JNIEnv *env, jclass clazz, jlong sessionPtr, jstring playlistUrlJ) {
// 将 sessionPtr 转换为 HlsSession 指针
HlsSession *sessionInput = (HlsSession *) (uintptr_t) sessionPtr;
// 通过全局列表查找该会话是否还存活
HlsSession *session = find_hls_session(sessionInput);
if (!session) {
return (*env)->NewStringUTF(env, "Session already freed");
}
// 如果已经关闭,则直接返回
if (session->closed) {
return (*env)->NewStringUTF(env, "Session already freed");
}
// 从 Java 字符串中获取播放列表路径(用于返回消息)
const char *playlistUrl = (*env)->GetStringUTFChars(env, playlistUrlJ, NULL);
// 写入 trailer,结束会话
int ret = av_write_trailer(session->ofmt_ctx);
if (ret < 0) {
(*env)->ReleaseStringUTFChars(env, playlistUrlJ, playlistUrl);
// 如果写 trailer 出错,也应关闭资源以避免泄漏
if (!(session->ofmt_ctx->oformat->flags & AVFMT_NOFILE))
avio_closep(&session->ofmt_ctx->pb);
avformat_free_context(session->ofmt_ctx);
free(session->ts_pattern);
// 从全局列表中删除该会话
remove_hls_session(session);
return (*env)->NewStringUTF(env, "Failed to write trailer");
}
// 关闭输出文件(如果必要),并释放输出上下文
if (!(session->ofmt_ctx->oformat->flags & AVFMT_NOFILE))
avio_closep(&session->ofmt_ctx->pb);
avformat_free_context(session->ofmt_ctx);
// 从全局列表中删除该会话
remove_hls_session(session);
// 标记会话状态为已关闭,以防重复释放
session->closed = 1;
// 释放会话中分配的资源
free(session->ts_pattern);
free(session);
// 构造返回消息
char resultMsg[256] = {0};
snprintf(resultMsg, sizeof(resultMsg),
"Persistent HLS session finished successfully: playlist %s", playlistUrl);
(*env)->ReleaseStringUTFChars(env, playlistUrlJ, playlistUrl);
return (*env)->NewStringUTF(env, resultMsg);
}
JNIEXPORT jstring JNICALL Java_com_litongjava_media_NativeMedia_appendVideoSegmentToHls
(JNIEnv *env, jclass clazz, jlong sessionPtr, jstring inputFilePathJ) {
HlsSession *session = (HlsSession *) sessionPtr;
if (!session || !session->ofmt_ctx) {
return (*env)->NewStringUTF(env, "Invalid HLS session pointer");
}
const char *inputFilePath = (*env)->GetStringUTFChars(env, inputFilePathJ, NULL);
if (!inputFilePath) {
return (*env)->NewStringUTF(env, "Failed to get input file path");
}
AVFormatContext *ifmt_ctx = NULL;
int ret = avformat_open_input(&ifmt_ctx, inputFilePath, NULL, NULL);
if (ret < 0) {
char errbuf[128] = {0};
av_strerror(ret, errbuf, sizeof(errbuf));
(*env)->ReleaseStringUTFChars(env, inputFilePathJ, inputFilePath);
char msg[256] = {0};
snprintf(msg, sizeof(msg), "Failed to open input file: %s", errbuf);
return (*env)->NewStringUTF(env, msg);
}
ret = avformat_find_stream_info(ifmt_ctx, NULL);
if (ret < 0) {
avformat_close_input(&ifmt_ctx);
(*env)->ReleaseStringUTFChars(env, inputFilePathJ, inputFilePath);
return (*env)->NewStringUTF(env, "Failed to retrieve stream info from input file");
}
// 如果 persistent HLS 会话中还没有输出流,则首次追加:
if (session->ofmt_ctx->nb_streams == 0) {
for (unsigned int i = 0; i < ifmt_ctx->nb_streams; i++) {
AVStream *in_stream = ifmt_ctx->streams[i];
if (in_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO ||
in_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
AVStream *out_stream = avformat_new_stream(session->ofmt_ctx, NULL);
if (!out_stream) continue;
ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
if (ret < 0) continue;
out_stream->codecpar->codec_tag = 0;
out_stream->time_base = in_stream->time_base;
}
}
// 使用保存的 opts 调用 avformat_write_header
ret = avformat_write_header(session->ofmt_ctx, &session->opts);
if (ret < 0) {
avformat_close_input(&ifmt_ctx);
(*env)->ReleaseStringUTFChars(env, inputFilePathJ, inputFilePath);
return (*env)->NewStringUTF(env, "Failed to write header on first segment append");
}
session->header_written = 1;
// 释放 opts 后不再需要
av_dict_free(&session->opts);
}
// 计算输入文件音视频流的最大时长(单位转换为 AV_TIME_BASE)
int64_t file_duration = 0;
for (unsigned int i = 0; i < ifmt_ctx->nb_streams; i++) {
AVStream *in_stream = ifmt_ctx->streams[i];
if (in_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO ||
in_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
if (in_stream->duration > 0) {
int64_t dur = av_rescale_q(in_stream->duration, in_stream->time_base, AV_TIME_BASE_Q);
if (dur > file_duration)
file_duration = dur;
}
}
}
AVPacket pkt;
while (av_read_frame(ifmt_ctx, &pkt) >= 0) {
AVStream *in_stream = ifmt_ctx->streams[pkt.stream_index];
if (in_stream->codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
in_stream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) {
av_packet_unref(&pkt);
continue;
}
// 按流类型匹配:假设只有一个视频流和一个音频流
int out_index = -1;
for (unsigned int j = 0; j < session->ofmt_ctx->nb_streams; j++) {
AVStream *out_stream = session->ofmt_ctx->streams[j];
if (out_stream->codecpar->codec_type == in_stream->codecpar->codec_type) {
out_index = j;
break;
}
}
if (out_index < 0) {
av_packet_unref(&pkt);
continue;
}
AVStream *out_stream = session->ofmt_ctx->streams[out_index];
// 将 pkt 时间戳转换后加上全局偏移
int64_t offset = av_rescale_q(session->global_offset, AV_TIME_BASE_Q, out_stream->time_base);
pkt.pts = av_rescale_q(pkt.pts, in_stream->time_base, out_stream->time_base) + offset;
pkt.dts = av_rescale_q(pkt.dts, in_stream->time_base, out_stream->time_base) + offset;
if (pkt.duration > 0)
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos = -1;
pkt.stream_index = out_index;
ret = av_interleaved_write_frame(session->ofmt_ctx, &pkt);
if (ret < 0) {
av_packet_unref(&pkt);
break;
}
av_packet_unref(&pkt);
}
// 累计更新全局时间偏移,确保下一个分段在时间上衔接
session->global_offset += file_duration;
avformat_close_input(&ifmt_ctx);
(*env)->ReleaseStringUTFChars(env, inputFilePathJ, inputFilePath);
char resultMsg[256] = {0};
snprintf(resultMsg, sizeof(resultMsg),
"Appended video segment successfully, updated global offset to %lld", session->global_offset);
return (*env)->NewStringUTF(env, resultMsg);
}
JNIEXPORT jstring JNICALL Java_com_litongjava_media_NativeMedia_listHlsSession
(JNIEnv *env, jclass clazz) {
// 构造 JSON 数组字符串,格式示例:
// [ {"sessionPtr":123456, "createdTime": "2025-04-12 09:30:00", "globalOffset": 1000}, {...} ]
char buffer[4096] = {0};
strcat(buffer, "[");
HlsSessionNode *curr = g_hlsSessionHead;
int first = 1;
char timeStr[64] = {0};
while (curr) {
if (!first) {
strcat(buffer, ",");
} else {
first = 0;
}
// 格式化创建时间为字符串
struct tm *tm_info = localtime(&(curr->session->created_time));
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", tm_info);
char item[256] = {0};
snprintf(item, sizeof(item),
"{\"sessionPtr\":%llu,\"createdTime\":\"%s\",\"globalOffset\":%lld}",
(unsigned long long)(uintptr_t)curr->session, timeStr, curr->session->global_offset);
strcat(buffer, item);
curr = curr->next;
}
strcat(buffer, "]");
return (*env)->NewStringUTF(env, buffer);
}
/* 新增:直接释放 HLS 会话,不需要传递播放列表路径 */
JNIEXPORT jstring JNICALL Java_com_litongjava_media_NativeMedia_freeHlsSession
(JNIEnv *env, jclass clazz, jlong sessionPtr) {
// 将 sessionPtr 转换为 HlsSession 指针
HlsSession *sessionInput = (HlsSession *) (uintptr_t) sessionPtr;
// 通过全局列表查找该会话是否还存活
HlsSession *session = find_hls_session(sessionInput);
if (!session) {
return (*env)->NewStringUTF(env, "Invalid session pointer");
}
// 如果已经关闭了,直接返回
if (session->closed) {
return (*env)->NewStringUTF(env, "Session already freed");
}
// 如果输出上下文存在且打开了输出文件,则关闭输出文件
if (session->ofmt_ctx) {
if (!(session->ofmt_ctx->oformat->flags & AVFMT_NOFILE))
avio_closep(&session->ofmt_ctx->pb);
avformat_free_context(session->ofmt_ctx);
}
// 从全局会话列表中删除该会话
remove_hls_session(session);
// 释放会话结构体中分配的资源
if (session->ts_pattern)
free(session->ts_pattern);
free(session);
return (*env)->NewStringUTF(env, "HLS session freed successfully");
}
三、说明与注意事项
Java 层说明:
NativeMedia
类定义了与 C 层交互的所有 native 方法,包括音视频转换、分段和持久化 HLS 会话相关的接口。HlsSessionCleaner
定时任务通过硬编码的正则表达式解析NativeMedia.listHlsSession()
返回的 JSON 信息,并在检测到会话已超过设定时间时调用NativeMedia.freeHlsSession
释放资源。在生产环境中应保证返回的 JSON 格式简单明确。
C 层说明:
HlsSession
结构体记录了输出上下文、分段参数、创建时间、关闭状态等信息。全局链表g_hlsSessionHead
用于管理所有活跃会话。- 初始化接口
initPersistentHls
创建输出上下文和配置选项字典后加入全局链表;追加分段接口appendVideoSegmentToHls
根据输入文件自动创建输出流(如果尚未创建),更新全局时间偏移,并写入 TS 分段; listHlsSession
将全局链表遍历后以 JSON 数组的格式返回给 Java 层;finishPersistentHls
和freeHlsSession
接口分别用于结束(写 trailer 后释放)和直接释放会话。其中,在释放时调用find_hls_session
确保当前会话还未被释放,以防止重复释放从而引起崩溃。
注意事项:
- 在多线程环境下,全局链表操作建议增加同步保护以避免并发冲突;
- 指针转换时使用了
(unsigned long long)(uintptr_t)
确保在 64 位系统中会话指针正确转换成无符号长整数,避免因指针大小不一致导致数值错误; - 定时任务中对 JSON 字符串的硬编码解析仅适用于简单格式,如遇格式变化建议使用成熟的 JSON 库(例如 Gson 或 Jackson)。
通过以上代码和说明,你可以完整实现持久化 HLS 会话模块,并在 Java 层通过定时任务自动监控和释放超时会话。所有代码均已保留,无省略部分,便于后续调试和扩展。
以上就是本模块的完整文档。如有疑问或需要进一步定制功能,请继续讨论。