All files / dingtalk-openclaw-connector/src/services/media video.ts

0% Statements 0/112
0% Branches 0/1
0% Functions 0/1
0% Lines 0/112

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                                                                                                                                                                                                                                                                                                   
/**
 * 视频处理模块
 * 支持视频元数据提取、封面生成、视频消息发送
 */
 
import type { Logger } from 'openclaw/plugin-sdk';
import type { DingtalkConfig } from '../../types/index.ts';
import { VIDEO_MARKER_PATTERN, toLocalPath, uploadMediaToDingTalk } from './common.ts';
 
/** 视频信息接口 */
export interface VideoInfo {
  path: string;
}
 
/**
 * 提取视频元数据(时长、分辨率)
 */
export async function extractVideoMetadata(
  filePath: string,
  log?: Logger,
): Promise<{ duration: number; width: number; height: number } | null> {
  try {
    const ffmpeg = require('fluent-ffmpeg');
    const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path;
    const ffprobePath = require('@ffprobe-installer/ffprobe').path;
    ffmpeg.setFfmpegPath(ffmpegPath);
    ffmpeg.setFfprobePath(ffprobePath);
 
    return new Promise((resolve) => {
      ffmpeg.ffprobe(filePath, (err: any, metadata: any) => {
        if (err) {
          log?.warn?.(`[DingTalk][Video] ffprobe 执行失败:${err.message}`);
          resolve(null);
          return;
        }
        try {
          const duration = metadata.format?.duration ? Math.floor(parseFloat(metadata.format.duration)) : 0;
          const videoStream = metadata.streams?.find((s: any) => s.codec_type === 'video');
          const width = videoStream?.width || 0;
          const height = videoStream?.height || 0;
          resolve({ duration, width, height });
        } catch (err) {
          log?.warn?.(`[DingTalk][Video] 解析 ffprobe 输出失败`);
          resolve(null);
        }
      });
    });
  } catch (err: any) {
    log?.warn?.(`[DingTalk][Video] 提取视频元数据失败:${err.message}`);
    return null;
  }
}
 
/**
 * 生成视频封面图(第 1 秒截图)
 */
export async function extractVideoThumbnail(
  videoPath: string,
  outputPath: string,
  log?: Logger,
): Promise<string | null> {
  try {
    const ffmpeg = require('fluent-ffmpeg');
    const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path;
    const path = await import('path');
    ffmpeg.setFfmpegPath(ffmpegPath);
 
    return new Promise((resolve) => {
      ffmpeg(videoPath)
        .screenshots({
          count: 1,
          folder: path.dirname(outputPath),
          filename: path.basename(outputPath),
          timemarks: ['1'],
          size: '?x360',
        })
        .on('end', () => {
          log?.info?.(`[DingTalk][Video] 封面生成成功:${outputPath}`);
          resolve(outputPath);
        })
        .on('error', (err: any) => {
          log?.error?.(`[DingTalk][Video] 封面生成失败:${err.message}`);
          resolve(null);
        });
    });
  } catch (err: any) {
    log?.error?.(`[DingTalk][Video] ffmpeg 失败:${err.message}`);
    return null;
  }
}
 
/**
 * 提取视频标记并发送视频消息
 */
export async function processVideoMarkers(
  content: string,
  sessionWebhook: string,
  config: DingtalkConfig,
  oapiToken: string | null,
  log?: Logger,
  useProactiveApi: boolean = false,
  target?: any,
): Promise<string> {
  const logPrefix = useProactiveApi ? '[DingTalk][Video][Proactive]' : '[DingTalk][Video]';
 
  if (!oapiToken) {
    log?.warn?.(`${logPrefix} 无 oapiToken,跳过视频处理`);
    return content;
  }
 
  const matches = [...content.matchAll(VIDEO_MARKER_PATTERN)];
  const videoInfos: VideoInfo[] = [];
  const invalidVideos: string[] = [];
 
  for (const match of matches) {
    try {
      const videoData = JSON.parse(match[1]);
      const rawPath = videoData.path;
      const absPath = toLocalPath(rawPath);
      videoInfos.push({ path: absPath });
    } catch (err) {
      log?.warn?.(`${logPrefix} 解析视频标记失败:${match[1]}`);
      invalidVideos.push(match[1]);
    }
  }
 
  if (videoInfos.length === 0) {
    return content;
  }
 
  log?.info?.(`${logPrefix} 检测到 ${videoInfos.length} 个视频,开始上传...`);
 
  let result = content;
  for (const videoInfo of videoInfos) {
    const mediaId = await uploadMediaToDingTalk(videoInfo.path, 'video', oapiToken, 20 * 1024 * 1024, log);
    if (mediaId) {
      result = result.replace(
        `[DINGTALK_VIDEO]${JSON.stringify({ path: videoInfo.path })}[/DINGTALK_VIDEO]`,
        `[视频已上传:${mediaId}]`,
      );
    }
  }
 
  return result;
}