344 lines
9.9 KiB
Objective-C
344 lines
9.9 KiB
Objective-C
//
|
|
// TTSPlaybackPipeline.m
|
|
// keyBoard
|
|
//
|
|
// Created by Mac on 2026/1/15.
|
|
//
|
|
|
|
#import "TTSPlaybackPipeline.h"
|
|
#import "AudioStreamPlayer.h"
|
|
#import <AVFoundation/AVFoundation.h>
|
|
|
|
@interface TTSPlaybackPipeline () <AudioStreamPlayerDelegate>
|
|
|
|
// 播放器
|
|
@property(nonatomic, strong) AVPlayer *urlPlayer;
|
|
@property(nonatomic, strong) AudioStreamPlayer *streamPlayer;
|
|
|
|
// 片段队列
|
|
@property(nonatomic, strong) NSMutableArray<NSDictionary *> *segmentQueue;
|
|
@property(nonatomic, strong)
|
|
NSMutableDictionary<NSString *, NSNumber *> *segmentDurations;
|
|
|
|
// 状态
|
|
@property(nonatomic, assign) BOOL playing;
|
|
@property(nonatomic, copy) NSString *currentSegmentId;
|
|
@property(nonatomic, strong) id playerTimeObserver;
|
|
|
|
// 队列
|
|
@property(nonatomic, strong) dispatch_queue_t playbackQueue;
|
|
|
|
@end
|
|
|
|
@implementation TTSPlaybackPipeline
|
|
|
|
- (instancetype)init {
|
|
self = [super init];
|
|
if (self) {
|
|
_segmentQueue = [[NSMutableArray alloc] init];
|
|
_segmentDurations = [[NSMutableDictionary alloc] init];
|
|
_playbackQueue = dispatch_queue_create("com.keyboard.aitalk.playback",
|
|
DISPATCH_QUEUE_SERIAL);
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[self stop];
|
|
}
|
|
|
|
#pragma mark - Public Methods
|
|
|
|
- (BOOL)start:(NSError **)error {
|
|
// 初始化 stream player
|
|
if (!self.streamPlayer) {
|
|
self.streamPlayer = [[AudioStreamPlayer alloc] init];
|
|
self.streamPlayer.delegate = self;
|
|
}
|
|
|
|
return [self.streamPlayer start:error];
|
|
}
|
|
|
|
- (void)stop {
|
|
dispatch_async(self.playbackQueue, ^{
|
|
// 停止 URL 播放
|
|
if (self.urlPlayer) {
|
|
[self.urlPlayer pause];
|
|
if (self.playerTimeObserver) {
|
|
[self.urlPlayer removeTimeObserver:self.playerTimeObserver];
|
|
self.playerTimeObserver = nil;
|
|
}
|
|
self.urlPlayer = nil;
|
|
}
|
|
|
|
// 停止流式播放
|
|
[self.streamPlayer stop];
|
|
|
|
// 清空队列
|
|
[self.segmentQueue removeAllObjects];
|
|
[self.segmentDurations removeAllObjects];
|
|
|
|
self.playing = NO;
|
|
self.currentSegmentId = nil;
|
|
});
|
|
}
|
|
|
|
- (void)enqueueURL:(NSURL *)url segmentId:(NSString *)segmentId {
|
|
if (!url || !segmentId)
|
|
return;
|
|
|
|
dispatch_async(self.playbackQueue, ^{
|
|
NSDictionary *segment = @{
|
|
@"type" : @(TTSPayloadTypeURL),
|
|
@"url" : url,
|
|
@"segmentId" : segmentId
|
|
};
|
|
[self.segmentQueue addObject:segment];
|
|
|
|
// 如果当前没有在播放,开始播放
|
|
if (!self.playing) {
|
|
[self playNextSegment];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)enqueueChunk:(NSData *)chunk
|
|
payloadType:(TTSPayloadType)type
|
|
segmentId:(NSString *)segmentId {
|
|
if (!chunk || !segmentId)
|
|
return;
|
|
|
|
dispatch_async(self.playbackQueue, ^{
|
|
switch (type) {
|
|
case TTSPayloadTypePCMChunk:
|
|
// 直接喂给 stream player
|
|
[self.streamPlayer enqueuePCMChunk:chunk
|
|
sampleRate:16000
|
|
channels:1
|
|
segmentId:segmentId];
|
|
|
|
if (!self.playing) {
|
|
self.playing = YES;
|
|
self.currentSegmentId = segmentId;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if ([self.delegate respondsToSelector:@selector
|
|
(pipelineDidStartSegment:duration:)]) {
|
|
[self.delegate pipelineDidStartSegment:segmentId duration:0];
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
|
|
case TTSPayloadTypeAACChunk:
|
|
// TODO: AAC 解码 -> PCM -> streamPlayer
|
|
NSLog(@"[TTSPlaybackPipeline] AAC chunk decoding not implemented yet");
|
|
break;
|
|
|
|
case TTSPayloadTypeOpusChunk:
|
|
// TODO: Opus 解码 -> PCM -> streamPlayer
|
|
NSLog(@"[TTSPlaybackPipeline] Opus chunk decoding not implemented yet");
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)markSegmentComplete:(NSString *)segmentId {
|
|
// Stream player 会自动处理播放完成
|
|
}
|
|
|
|
- (NSTimeInterval)currentTimeForSegment:(NSString *)segmentId {
|
|
if (![segmentId isEqualToString:self.currentSegmentId]) {
|
|
return 0;
|
|
}
|
|
|
|
if (self.urlPlayer) {
|
|
return CMTimeGetSeconds(self.urlPlayer.currentTime);
|
|
}
|
|
|
|
return [self.streamPlayer playbackTimeForSegment:segmentId];
|
|
}
|
|
|
|
- (NSTimeInterval)durationForSegment:(NSString *)segmentId {
|
|
NSNumber *duration = self.segmentDurations[segmentId];
|
|
if (duration) {
|
|
return duration.doubleValue;
|
|
}
|
|
|
|
if (self.urlPlayer && [segmentId isEqualToString:self.currentSegmentId]) {
|
|
CMTime duration = self.urlPlayer.currentItem.duration;
|
|
if (CMTIME_IS_VALID(duration)) {
|
|
return CMTimeGetSeconds(duration);
|
|
}
|
|
}
|
|
|
|
return [self.streamPlayer durationForSegment:segmentId];
|
|
}
|
|
|
|
#pragma mark - Private Methods
|
|
|
|
- (void)playNextSegment {
|
|
if (self.segmentQueue.count == 0) {
|
|
self.playing = NO;
|
|
self.currentSegmentId = nil;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if ([self.delegate
|
|
respondsToSelector:@selector(pipelineDidFinishAllSegments)]) {
|
|
[self.delegate pipelineDidFinishAllSegments];
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
NSDictionary *segment = self.segmentQueue.firstObject;
|
|
[self.segmentQueue removeObjectAtIndex:0];
|
|
|
|
TTSPayloadType type = [segment[@"type"] integerValue];
|
|
NSString *segmentId = segment[@"segmentId"];
|
|
|
|
self.playing = YES;
|
|
self.currentSegmentId = segmentId;
|
|
|
|
if (type == TTSPayloadTypeURL) {
|
|
NSURL *url = segment[@"url"];
|
|
[self playURL:url segmentId:segmentId];
|
|
}
|
|
}
|
|
|
|
- (void)playURL:(NSURL *)url segmentId:(NSString *)segmentId {
|
|
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url];
|
|
|
|
if (!self.urlPlayer) {
|
|
self.urlPlayer = [AVPlayer playerWithPlayerItem:item];
|
|
} else {
|
|
[self.urlPlayer replaceCurrentItemWithPlayerItem:item];
|
|
}
|
|
|
|
// 监听播放完成
|
|
[[NSNotificationCenter defaultCenter]
|
|
addObserver:self
|
|
selector:@selector(playerItemDidFinish:)
|
|
name:AVPlayerItemDidPlayToEndTimeNotification
|
|
object:item];
|
|
|
|
// 添加时间观察器
|
|
__weak typeof(self) weakSelf = self;
|
|
self.playerTimeObserver = [self.urlPlayer
|
|
addPeriodicTimeObserverForInterval:CMTimeMake(1, 30)
|
|
queue:dispatch_get_main_queue()
|
|
usingBlock:^(CMTime time) {
|
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
if (!strongSelf)
|
|
return;
|
|
|
|
NSTimeInterval currentTime =
|
|
CMTimeGetSeconds(time);
|
|
if ([strongSelf.delegate
|
|
respondsToSelector:@selector
|
|
(pipelineDidUpdatePlaybackTime:
|
|
segmentId:)]) {
|
|
[strongSelf.delegate
|
|
pipelineDidUpdatePlaybackTime:currentTime
|
|
segmentId:segmentId];
|
|
}
|
|
}];
|
|
|
|
// 等待资源加载后获取时长并开始播放
|
|
[item.asset
|
|
loadValuesAsynchronouslyForKeys:@[ @"duration" ]
|
|
completionHandler:^{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
NSTimeInterval duration =
|
|
CMTimeGetSeconds(item.duration);
|
|
if (!isnan(duration)) {
|
|
self.segmentDurations[segmentId] = @(duration);
|
|
}
|
|
|
|
if ([self.delegate respondsToSelector:@selector
|
|
(pipelineDidStartSegment:
|
|
duration:)]) {
|
|
[self.delegate pipelineDidStartSegment:segmentId
|
|
duration:duration];
|
|
}
|
|
|
|
[self.urlPlayer play];
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (void)playerItemDidFinish:(NSNotification *)notification {
|
|
[[NSNotificationCenter defaultCenter]
|
|
removeObserver:self
|
|
name:AVPlayerItemDidPlayToEndTimeNotification
|
|
object:notification.object];
|
|
|
|
if (self.playerTimeObserver) {
|
|
[self.urlPlayer removeTimeObserver:self.playerTimeObserver];
|
|
self.playerTimeObserver = nil;
|
|
}
|
|
|
|
NSString *finishedSegmentId = self.currentSegmentId;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if ([self.delegate
|
|
respondsToSelector:@selector(pipelineDidFinishSegment:)]) {
|
|
[self.delegate pipelineDidFinishSegment:finishedSegmentId];
|
|
}
|
|
});
|
|
|
|
dispatch_async(self.playbackQueue, ^{
|
|
[self playNextSegment];
|
|
});
|
|
}
|
|
|
|
#pragma mark - AudioStreamPlayerDelegate
|
|
|
|
- (void)audioStreamPlayerDidStartSegment:(NSString *)segmentId {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if ([self.delegate
|
|
respondsToSelector:@selector(pipelineDidStartSegment:duration:)]) {
|
|
[self.delegate pipelineDidStartSegment:segmentId duration:0];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)audioStreamPlayerDidUpdateTime:(NSTimeInterval)time
|
|
segmentId:(NSString *)segmentId {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if ([self.delegate respondsToSelector:@selector
|
|
(pipelineDidUpdatePlaybackTime:segmentId:)]) {
|
|
[self.delegate pipelineDidUpdatePlaybackTime:time segmentId:segmentId];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)audioStreamPlayerDidFinishSegment:(NSString *)segmentId {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if ([self.delegate
|
|
respondsToSelector:@selector(pipelineDidFinishSegment:)]) {
|
|
[self.delegate pipelineDidFinishSegment:segmentId];
|
|
}
|
|
});
|
|
|
|
dispatch_async(self.playbackQueue, ^{
|
|
// 检查是否还有更多片段
|
|
if (self.segmentQueue.count == 0) {
|
|
self.playing = NO;
|
|
self.currentSegmentId = nil;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if ([self.delegate
|
|
respondsToSelector:@selector(pipelineDidFinishAllSegments)]) {
|
|
[self.delegate pipelineDidFinishAllSegments];
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
@end
|