持久化 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 层通过定时任务自动监控和释放超时会话。所有代码均已保留,无省略部分,便于后续调试和扩展。
以上就是本模块的完整文档。如有疑问或需要进一步定制功能,请继续讨论。
