@lynker-desktop/electron-sdk 0.0.9-alpha.3 → 0.0.9-alpha.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/main/index.js CHANGED
@@ -5,6 +5,7 @@ const path = require('node:path');
5
5
  const node_util = require('node:util');
6
6
  const http = require('node:http');
7
7
  const https = require('node:https');
8
+ const node_child_process = require('node:child_process');
8
9
  const electron = require('electron');
9
10
  const md5 = require('md5');
10
11
  const uuid = require('uuid');
@@ -56,7 +57,7 @@ function findWebContentsByPid(pid) {
56
57
  * @returns
57
58
  */
58
59
  const initialize = (options) => {
59
- let { protocol, preload, loadingViewUrl, serveOptions = [], errorViewUrl, preloadWebContentsUrl, webviewDomainWhiteList, resourceCacheOptions } = options;
60
+ let { protocol, preload, loadingViewUrl, serveOptions = [], errorViewUrl, preloadWebContentsConfig, webviewDomainWhiteList, resourceCacheOptions } = options;
60
61
  if (exports.isInitialized) {
61
62
  // @ts-ignore
62
63
  return global['__GLBOAL_ELECTRON_SDK__'];
@@ -64,7 +65,10 @@ const initialize = (options) => {
64
65
  exports.isInitialized = true;
65
66
  ipc__namespace.initialize();
66
67
  logs__namespace.initialize();
67
- const wm = windowManager__namespace.initialize(preload, loadingViewUrl, errorViewUrl, preloadWebContentsUrl, webviewDomainWhiteList || []);
68
+ if (options.ffmpegPath) {
69
+ VideoDownloader.getInstance().setFFmpegPath(options.ffmpegPath);
70
+ }
71
+ const wm = windowManager__namespace.initialize(preload, loadingViewUrl, errorViewUrl, preloadWebContentsConfig, webviewDomainWhiteList || []);
68
72
  if (protocol) {
69
73
  registerProtocol(protocol);
70
74
  }
@@ -92,6 +96,10 @@ const initialize = (options) => {
92
96
  SDK.ipc = ipc__namespace.mainIPC;
93
97
  SDK.windowManager = wm;
94
98
  SDK.getPreload = () => preload;
99
+ SDK.downloadVideo = downloadVideo;
100
+ SDK.clearVideoCache = clearVideoCache;
101
+ SDK.getVideoCacheStats = getVideoCacheStats;
102
+ SDK.removeVideoCache = removeVideoCache;
95
103
  ipc__namespace.mainIPC.handleRenderer(index$1.IPC_GET_APP_METRICS, async () => {
96
104
  const metrics = electron.app.getAppMetrics().map(i => {
97
105
  const webContents = findWebContentsByPid(i.pid || 0);
@@ -104,6 +112,30 @@ const initialize = (options) => {
104
112
  });
105
113
  return JSON.stringify(metrics);
106
114
  });
115
+ ipc__namespace.mainIPC.handleRenderer(`__sdk_transcode_video__`, async (options) => {
116
+ const md5Options = md5(JSON.stringify(options.url));
117
+ downloadVideo(options, {
118
+ onDownloadProgress: (progress) => {
119
+ ipc__namespace.mainIPC.invokeAllRenderer(`__sdk_transcode_video_callback_${md5Options}__`, {
120
+ type: 'downloadProgress',
121
+ data: progress
122
+ });
123
+ },
124
+ onTranscodeProgress: (progress) => {
125
+ ipc__namespace.mainIPC.invokeAllRenderer(`__sdk_transcode_video_callback_${md5Options}__`, {
126
+ type: 'transcodeProgress',
127
+ data: progress
128
+ });
129
+ },
130
+ onComplete: (result) => {
131
+ ipc__namespace.mainIPC.invokeAllRenderer(`__sdk_transcode_video_callback_${md5Options}__`, {
132
+ type: 'onComplete',
133
+ data: result
134
+ });
135
+ },
136
+ });
137
+ return true;
138
+ });
107
139
  // @ts-ignore
108
140
  global['__GLBOAL_ELECTRON_SDK__'] = SDK;
109
141
  return SDK;
@@ -484,6 +516,463 @@ function getRandomUUID(key = `${Date.now()}`) {
484
516
  const uuid$1 = uuid.v5(`${JSON.stringify(key)}_${Date.now()}_${randomValue}`, uuid.v5.URL);
485
517
  return uuid$1;
486
518
  }
519
+ /**
520
+ * 视频下载和转码类
521
+ */
522
+ class VideoDownloader {
523
+ constructor() {
524
+ this.ffmpegPath = '';
525
+ this.defaultCacheTTL = 24 * 60 * 60 * 1000; // 24小时缓存有效期
526
+ }
527
+ static getInstance() {
528
+ if (!VideoDownloader.instance) {
529
+ VideoDownloader.instance = new VideoDownloader();
530
+ }
531
+ return VideoDownloader.instance;
532
+ }
533
+ setFFmpegPath(ffmpegPath) {
534
+ this.ffmpegPath = ffmpegPath;
535
+ }
536
+ /**
537
+ * 下载视频文件
538
+ */
539
+ async downloadVideoFile(url, outputPath, onProgress) {
540
+ return new Promise((resolve, reject) => {
541
+ const lib = url.startsWith('https') ? https : http;
542
+ const file = fs.createWriteStream(outputPath);
543
+ let downloadedBytes = 0;
544
+ let totalBytes = 0;
545
+ let startTime = Date.now();
546
+ let lastTime = startTime;
547
+ let lastBytes = 0;
548
+ const cleanup = () => {
549
+ if (file) {
550
+ file.close();
551
+ fs.unlinkSync(outputPath);
552
+ }
553
+ };
554
+ const request = lib.get(url, (res) => {
555
+ if (res.statusCode !== 200) {
556
+ cleanup();
557
+ reject(new Error(`下载失败,状态码: ${res.statusCode}`));
558
+ return;
559
+ }
560
+ totalBytes = parseInt(res.headers['content-length'] || '0', 10);
561
+ res.on('data', (chunk) => {
562
+ downloadedBytes += chunk.length;
563
+ const now = Date.now();
564
+ if (onProgress && (now - lastTime > 100 || downloadedBytes === totalBytes)) {
565
+ const speed = ((downloadedBytes - lastBytes) * 1000) / (now - lastTime);
566
+ onProgress({
567
+ downloaded: downloadedBytes,
568
+ total: totalBytes,
569
+ percentage: totalBytes > 0 ? (downloadedBytes / totalBytes) * 100 : 0,
570
+ speed: speed || 0
571
+ });
572
+ lastTime = now;
573
+ lastBytes = downloadedBytes;
574
+ }
575
+ });
576
+ res.pipe(file);
577
+ file.on('finish', () => {
578
+ file.close();
579
+ resolve();
580
+ });
581
+ file.on('error', (err) => {
582
+ cleanup();
583
+ reject(err);
584
+ });
585
+ });
586
+ request.on('error', (err) => {
587
+ cleanup();
588
+ reject(err);
589
+ });
590
+ });
591
+ }
592
+ /**
593
+ * 使用FFmpeg转码视频
594
+ */
595
+ async transcodeVideo(inputPath, outputPath, options, onProgress) {
596
+ return new Promise((resolve, reject) => {
597
+ const ffmpegPath = options.ffmpegPath || this.ffmpegPath;
598
+ if (!ffmpegPath) {
599
+ reject(new Error('FFmpeg路径未设置'));
600
+ return;
601
+ }
602
+ const format = options.format || 'mp4';
603
+ const quality = options.quality || 18;
604
+ const videoCodec = options.videoCodec || 'h264';
605
+ const audioCodec = options.audioCodec || 'aac';
606
+ // 根据格式选择合适的编码器
607
+ const getCodecForFormat = (format, videoCodec, audioCodec) => {
608
+ switch (format) {
609
+ case 'webm':
610
+ return {
611
+ videoCodec: videoCodec === 'h264' ? 'libvpx-vp9' : videoCodec,
612
+ audioCodec: audioCodec === 'aac' ? 'libopus' : audioCodec
613
+ };
614
+ case 'avi':
615
+ return {
616
+ videoCodec: videoCodec === 'h265' ? 'h264' : videoCodec, // AVI不支持H265
617
+ audioCodec: audioCodec === 'opus' ? 'mp3' : audioCodec
618
+ };
619
+ case 'mov':
620
+ return {
621
+ videoCodec: videoCodec === 'vp9' ? 'h264' : videoCodec, // MOV对VP9支持有限
622
+ audioCodec: audioCodec === 'opus' ? 'aac' : audioCodec
623
+ };
624
+ default: // mp4, mkv
625
+ return { videoCodec, audioCodec };
626
+ }
627
+ };
628
+ const { videoCodec: finalVideoCodec, audioCodec: finalAudioCodec } = getCodecForFormat(format, videoCodec, audioCodec);
629
+ // 构建FFmpeg命令
630
+ const args = [
631
+ '-i', inputPath,
632
+ '-c:v', finalVideoCodec,
633
+ '-c:a', finalAudioCodec,
634
+ '-crf', quality.toString(),
635
+ '-preset', 'medium',
636
+ '-f', format, // 指定输出格式
637
+ '-y', // 覆盖输出文件
638
+ outputPath
639
+ ];
640
+ const ffmpegProcess = node_child_process.spawn(ffmpegPath, args);
641
+ let duration = 0;
642
+ let stderr = '';
643
+ ffmpegProcess.stderr.on('data', (data) => {
644
+ stderr += data.toString();
645
+ // 解析FFmpeg输出获取进度
646
+ if (onProgress) {
647
+ const durationMatch = stderr.match(/Duration: (\d{2}):(\d{2}):(\d{2})\.(\d{2})/);
648
+ if (durationMatch && duration === 0) {
649
+ const hours = parseInt(durationMatch[1] || '0', 10);
650
+ const minutes = parseInt(durationMatch[2] || '0', 10);
651
+ const seconds = parseInt(durationMatch[3] || '0', 10);
652
+ const centiseconds = parseInt(durationMatch[4] || '0', 10);
653
+ duration = hours * 3600 + minutes * 60 + seconds + centiseconds / 100;
654
+ }
655
+ const timeMatch = stderr.match(/time=(\d{2}):(\d{2}):(\d{2})\.(\d{2})/);
656
+ if (timeMatch && duration > 0) {
657
+ const hours = parseInt(timeMatch[1] || '0', 10);
658
+ const minutes = parseInt(timeMatch[2] || '0', 10);
659
+ const seconds = parseInt(timeMatch[3] || '0', 10);
660
+ const centiseconds = parseInt(timeMatch[4] || '0', 10);
661
+ const currentTime = hours * 3600 + minutes * 60 + seconds + centiseconds / 100;
662
+ const percentage = (currentTime / duration) * 100;
663
+ const speedMatch = stderr.match(/speed=([\d.]+)x/);
664
+ const speed = speedMatch ? parseFloat(speedMatch[1] || '1') : 1;
665
+ onProgress({
666
+ percentage: Math.min(percentage, 100),
667
+ time: currentTime,
668
+ speed: speed
669
+ });
670
+ }
671
+ }
672
+ });
673
+ ffmpegProcess.on('close', (code) => {
674
+ if (code === 0) {
675
+ resolve();
676
+ }
677
+ else {
678
+ reject(new Error(`FFmpeg转码失败,退出码: ${code}\n${stderr}`));
679
+ }
680
+ });
681
+ ffmpegProcess.on('error', (err) => {
682
+ reject(new Error(`FFmpeg执行失败: ${err.message}`));
683
+ });
684
+ });
685
+ }
686
+ /**
687
+ * 检查FFmpeg是否可用
688
+ */
689
+ async checkFFmpegAvailable(ffmpegPath) {
690
+ return new Promise((resolve) => {
691
+ const command = ffmpegPath || this.ffmpegPath;
692
+ if (!command) {
693
+ resolve(false);
694
+ return;
695
+ }
696
+ node_child_process.exec(`${command} -version`, (error) => {
697
+ resolve(!error);
698
+ });
699
+ });
700
+ }
701
+ /**
702
+ * 获取视频信息
703
+ */
704
+ async getVideoInfo(inputPath, ffmpegPath) {
705
+ return new Promise((resolve, reject) => {
706
+ const ffmpegPath_cmd = ffmpegPath || this.ffmpegPath;
707
+ const args = ['-i', inputPath, '-f', 'null', '-'];
708
+ if (!ffmpegPath_cmd) {
709
+ resolve({ duration: 0, fileSize: 0 });
710
+ return;
711
+ }
712
+ const ffmpegProcess = node_child_process.spawn(ffmpegPath_cmd, args);
713
+ let stderr = '';
714
+ let duration = 0;
715
+ ffmpegProcess.stderr.on('data', (data) => {
716
+ stderr += data.toString();
717
+ const durationMatch = stderr.match(/Duration: (\d{2}):(\d{2}):(\d{2})\.(\d{2})/);
718
+ if (durationMatch) {
719
+ const hours = parseInt(durationMatch[1] || '0', 10);
720
+ const minutes = parseInt(durationMatch[2] || '0', 10);
721
+ const seconds = parseInt(durationMatch[3] || '0', 10);
722
+ const centiseconds = parseInt(durationMatch[4] || '0', 10);
723
+ duration = hours * 3600 + minutes * 60 + seconds + centiseconds / 100;
724
+ }
725
+ });
726
+ ffmpegProcess.on('close', () => {
727
+ const stats = fs.statSync(inputPath);
728
+ resolve({
729
+ duration,
730
+ fileSize: stats.size
731
+ });
732
+ });
733
+ ffmpegProcess.on('error', (err) => {
734
+ reject(err);
735
+ });
736
+ });
737
+ }
738
+ /**
739
+ * 生成缓存文件名(基于URL的MD5)
740
+ */
741
+ generateCacheFileName(options) {
742
+ const format = options.format || 'mp4';
743
+ const urlMd5 = md5(options.url);
744
+ return `${urlMd5}.${format}`;
745
+ }
746
+ /**
747
+ * 检查缓存文件是否存在
748
+ */
749
+ isCacheFileExists(cacheFilePath) {
750
+ console.log('检查缓存文件是否存在: ', cacheFilePath);
751
+ return fs.existsSync(cacheFilePath);
752
+ }
753
+ /**
754
+ * 清理过期缓存文件
755
+ */
756
+ cleanExpiredCacheFiles(outputDir, cacheTTL = this.defaultCacheTTL) {
757
+ try {
758
+ const files = fs.readdirSync(outputDir);
759
+ const now = Date.now();
760
+ files.forEach(file => {
761
+ const filePath = path.join(outputDir, file);
762
+ try {
763
+ const stats = fs.statSync(filePath);
764
+ // 如果文件超过缓存时间,删除它
765
+ if (now - stats.mtimeMs > cacheTTL) {
766
+ fs.unlinkSync(filePath);
767
+ console.log(`🧹 删除过期缓存文件: ${file}`);
768
+ }
769
+ }
770
+ catch (error) {
771
+ // 忽略单个文件错误
772
+ }
773
+ });
774
+ }
775
+ catch (error) {
776
+ // 忽略目录读取错误
777
+ }
778
+ }
779
+ /**
780
+ * 下载并转码视频
781
+ */
782
+ async downloadAndTranscode(options, callbacks) {
783
+ try {
784
+ const outputDir = options.outputDir || electron.app.getPath('temp');
785
+ // 获取缓存配置
786
+ const cacheConfig = options.cache || { enabled: true, ttl: this.defaultCacheTTL };
787
+ const cacheEnabled = cacheConfig.enabled !== false;
788
+ const cacheTTL = cacheConfig.ttl || this.defaultCacheTTL;
789
+ // 确保输出目录存在
790
+ if (!fs.existsSync(outputDir)) {
791
+ fs.mkdirSync(outputDir, { recursive: true });
792
+ }
793
+ // 清理过期缓存文件
794
+ this.cleanExpiredCacheFiles(outputDir, cacheTTL);
795
+ // 生成缓存文件名
796
+ const cacheFileName = this.generateCacheFileName(options);
797
+ const cacheFilePath = path.join(outputDir, cacheFileName);
798
+ // 如果启用缓存,检查缓存文件是否存在
799
+ if (cacheEnabled && this.isCacheFileExists(cacheFilePath)) {
800
+ console.log('🎯 命中缓存,直接返回缓存文件');
801
+ // 获取文件信息
802
+ const stats = fs.statSync(cacheFilePath);
803
+ const result = {
804
+ success: true,
805
+ outputPath: cacheFilePath,
806
+ fileSize: stats.size,
807
+ fromCache: true
808
+ };
809
+ callbacks?.onComplete?.({
810
+ success: true,
811
+ outputPath: cacheFilePath
812
+ });
813
+ return result;
814
+ }
815
+ // 检查FFmpeg是否可用
816
+ const ffmpegAvailable = await this.checkFFmpegAvailable(options.ffmpegPath);
817
+ if (!ffmpegAvailable) {
818
+ throw new Error('FFmpeg不可用,请确保已安装FFmpeg或提供正确的路径');
819
+ }
820
+ // 生成临时文件名和最终输出路径
821
+ const fileName = options.outputName || md5(options.url);
822
+ const format = options.format || 'mp4';
823
+ const tempPath = path.join(outputDir, `${fileName}.temp`);
824
+ const finalOutputPath = path.join(outputDir, `${fileName}.${format}`);
825
+ const outputPath = cacheEnabled ? cacheFilePath : finalOutputPath;
826
+ console.log('📥 开始下载视频...');
827
+ // 下载视频
828
+ await this.downloadVideoFile(options.url, tempPath, callbacks?.onDownloadProgress);
829
+ console.log('🔄 开始转码视频...');
830
+ // 转码视频
831
+ await this.transcodeVideo(tempPath, outputPath, {
832
+ ...options,
833
+ outputDir
834
+ }, callbacks?.onTranscodeProgress);
835
+ // 如果启用了缓存,将缓存文件复制到最终输出路径
836
+ if (cacheEnabled && outputPath !== finalOutputPath) {
837
+ fs.copyFileSync(outputPath, finalOutputPath);
838
+ console.log('📋 缓存文件已复制到最终输出路径');
839
+ }
840
+ // 获取视频信息
841
+ const videoInfo = await this.getVideoInfo(outputPath, options.ffmpegPath);
842
+ // 清理临时文件
843
+ if (!options.keepOriginal) {
844
+ fs.unlinkSync(tempPath);
845
+ }
846
+ console.log('💾 文件已保存到缓存');
847
+ const result = {
848
+ success: true,
849
+ outputPath: cacheEnabled ? finalOutputPath : outputPath,
850
+ originalPath: options.keepOriginal ? tempPath : undefined,
851
+ duration: videoInfo.duration,
852
+ fileSize: videoInfo.fileSize,
853
+ fromCache: false
854
+ };
855
+ callbacks?.onComplete?.({
856
+ success: true,
857
+ outputPath: cacheEnabled ? finalOutputPath : outputPath
858
+ });
859
+ return result;
860
+ }
861
+ catch (error) {
862
+ const errorMessage = error instanceof Error ? error.message : '未知错误';
863
+ const result = {
864
+ success: false,
865
+ error: errorMessage
866
+ };
867
+ callbacks?.onComplete?.({
868
+ success: false,
869
+ error: errorMessage
870
+ });
871
+ return result;
872
+ }
873
+ }
874
+ /**
875
+ * 清除指定目录的所有缓存文件
876
+ */
877
+ clearCache(outputDir) {
878
+ try {
879
+ const files = fs.readdirSync(outputDir);
880
+ files.forEach(file => {
881
+ const filePath = path.join(outputDir, file);
882
+ try {
883
+ fs.unlinkSync(filePath);
884
+ }
885
+ catch (error) {
886
+ // 忽略单个文件删除错误
887
+ }
888
+ });
889
+ console.log('🧹 缓存文件已清空');
890
+ }
891
+ catch (error) {
892
+ console.log('⚠️ 清理缓存时发生错误:', error instanceof Error ? error.message : '未知错误');
893
+ }
894
+ }
895
+ /**
896
+ * 获取缓存统计信息
897
+ */
898
+ getCacheStats(outputDir) {
899
+ try {
900
+ const files = fs.readdirSync(outputDir);
901
+ const entries = files.map(file => {
902
+ const filePath = path.join(outputDir, file);
903
+ try {
904
+ const stats = fs.statSync(filePath);
905
+ return {
906
+ fileName: file,
907
+ filePath: filePath,
908
+ fileSize: stats.size,
909
+ mtime: stats.mtimeMs
910
+ };
911
+ }
912
+ catch {
913
+ return null;
914
+ }
915
+ }).filter((entry) => entry !== null);
916
+ return {
917
+ size: entries.length,
918
+ entries
919
+ };
920
+ }
921
+ catch (error) {
922
+ return { size: 0, entries: [] };
923
+ }
924
+ }
925
+ /**
926
+ * 删除指定URL的缓存文件
927
+ */
928
+ removeCache(options) {
929
+ try {
930
+ const cacheFileName = this.generateCacheFileName(options);
931
+ const cacheFilePath = path.join(options.outputDir, cacheFileName);
932
+ if (fs.existsSync(cacheFilePath)) {
933
+ fs.unlinkSync(cacheFilePath);
934
+ console.log(`🗑️ 删除缓存文件: ${cacheFileName}`);
935
+ return true;
936
+ }
937
+ return false;
938
+ }
939
+ catch (error) {
940
+ console.log('⚠️ 删除缓存文件时发生错误:', error instanceof Error ? error.message : '未知错误');
941
+ return false;
942
+ }
943
+ }
944
+ }
945
+ /**
946
+ * 下载视频并转码
947
+ * @param options 下载和转码选项
948
+ * @param callbacks 进度回调
949
+ * @returns 转码结果
950
+ */
951
+ const downloadVideo = async (options, callbacks) => {
952
+ const downloader = VideoDownloader.getInstance();
953
+ return downloader.downloadAndTranscode(options, callbacks);
954
+ };
955
+ /**
956
+ * 清除指定目录的所有缓存文件
957
+ */
958
+ const clearVideoCache = (outputDir) => {
959
+ const downloader = VideoDownloader.getInstance();
960
+ downloader.clearCache(outputDir);
961
+ };
962
+ /**
963
+ * 获取缓存统计信息
964
+ */
965
+ const getVideoCacheStats = (outputDir) => {
966
+ const downloader = VideoDownloader.getInstance();
967
+ return downloader.getCacheStats(outputDir);
968
+ };
969
+ /**
970
+ * 删除指定URL的缓存文件
971
+ */
972
+ const removeVideoCache = (options) => {
973
+ const downloader = VideoDownloader.getInstance();
974
+ return downloader.removeCache(options);
975
+ };
487
976
  /**
488
977
  * 获取定制Session
489
978
  */
@@ -506,12 +995,16 @@ const index = new Proxy({}, {
506
995
  });
507
996
 
508
997
  exports.ResourceCache = ResourceCache;
998
+ exports.clearVideoCache = clearVideoCache;
509
999
  exports.default = index;
1000
+ exports.downloadVideo = downloadVideo;
510
1001
  exports.enable = enable;
511
1002
  exports.getCustomSession = getCustomSession;
512
1003
  exports.getRandomUUID = getRandomUUID;
513
1004
  exports.getSDK = getSDK;
1005
+ exports.getVideoCacheStats = getVideoCacheStats;
514
1006
  exports.initialize = initialize;
515
1007
  exports.registerProtocol = registerProtocol;
1008
+ exports.removeVideoCache = removeVideoCache;
516
1009
  exports.serve = serve;
517
1010
  //# sourceMappingURL=index.js.map