Bläddra i källkod

player offset

Steven 1 år sedan
förälder
incheckning
8eddfd0af0

+ 6 - 0
KulexiuForTeacher/KulexiuForTeacher.xcodeproj/project.pbxproj

@@ -632,6 +632,7 @@
 		BC31BFA02B219C5700F7D538 /* WidgetDotView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC31BF712B219C5700F7D538 /* WidgetDotView.xib */; };
 		BC31BFA42B219C5700F7D538 /* WidgetSpeedView.m in Sources */ = {isa = PBXBuildFile; fileRef = BC31BF792B219C5700F7D538 /* WidgetSpeedView.m */; };
 		BC31BFA52B219C5700F7D538 /* WidgetBottomButtonView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC31BF7B2B219C5700F7D538 /* WidgetBottomButtonView.xib */; };
+		BC31BFAB2B21AFBF00F7D538 /* AVPlayer+KSSeekSmoothly.m in Sources */ = {isa = PBXBuildFile; fileRef = BC31BFA92B21AFBF00F7D538 /* AVPlayer+KSSeekSmoothly.m */; };
 		BC32E109286AB142001434DD /* BaseAlertView.m in Sources */ = {isa = PBXBuildFile; fileRef = BC32E108286AB142001434DD /* BaseAlertView.m */; };
 		BC32E10C286AB31C001434DD /* KSPublicAlertView.m in Sources */ = {isa = PBXBuildFile; fileRef = BC32E10B286AB31C001434DD /* KSPublicAlertView.m */; };
 		BC32E10E286AB326001434DD /* KSPublicAlertView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC32E10D286AB326001434DD /* KSPublicAlertView.xib */; };
@@ -2399,6 +2400,8 @@
 		BC31BF7C2B219C5700F7D538 /* WidgetNavView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WidgetNavView.h; sourceTree = "<group>"; };
 		BC31BF7D2B219C5700F7D538 /* WidgetFunctionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WidgetFunctionView.h; sourceTree = "<group>"; };
 		BC31BF7E2B219C5700F7D538 /* WidgetDotView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WidgetDotView.h; sourceTree = "<group>"; };
+		BC31BFA92B21AFBF00F7D538 /* AVPlayer+KSSeekSmoothly.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "AVPlayer+KSSeekSmoothly.m"; sourceTree = "<group>"; };
+		BC31BFAA2B21AFBF00F7D538 /* AVPlayer+KSSeekSmoothly.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "AVPlayer+KSSeekSmoothly.h"; sourceTree = "<group>"; };
 		BC32E107286AB142001434DD /* BaseAlertView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BaseAlertView.h; sourceTree = "<group>"; };
 		BC32E108286AB142001434DD /* BaseAlertView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BaseAlertView.m; sourceTree = "<group>"; };
 		BC32E10A286AB31C001434DD /* KSPublicAlertView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KSPublicAlertView.h; sourceTree = "<group>"; };
@@ -6138,6 +6141,8 @@
 			children = (
 				BC38C4202AF900E100ABFCC2 /* kSNewPlayer.h */,
 				BC38C4212AF900E100ABFCC2 /* kSNewPlayer.m */,
+				BC31BFAA2B21AFBF00F7D538 /* AVPlayer+KSSeekSmoothly.h */,
+				BC31BFA92B21AFBF00F7D538 /* AVPlayer+KSSeekSmoothly.m */,
 			);
 			path = MusicPlayer;
 			sourceTree = "<group>";
@@ -9032,6 +9037,7 @@
 				2780C92227E4902800A95A4F /* PasswordBodyView.m in Sources */,
 				BC38C4252AF900E100ABFCC2 /* KSNewAlertView.m in Sources */,
 				275E8A6927E18F2300DD3F6E /* AppDelegate.m in Sources */,
+				BC31BFAB2B21AFBF00F7D538 /* AVPlayer+KSSeekSmoothly.m in Sources */,
 				2779361E27E3338E0010E277 /* KSUpdateManager.m in Sources */,
 				BCC9F42827F69BD200647449 /* WhiteUtils.m in Sources */,
 				BC106BB42A8F4BC9000759A9 /* LiveModuleService.m in Sources */,

+ 1 - 1
KulexiuForTeacher/KulexiuForTeacher/AppDelegate.m

@@ -159,7 +159,7 @@
         else {
             [KSNetworkingManager configRequestHeader];
             NSInteger tenantId = [UserDefaultObjectForKey(TENANT_ID) integerValue];
-            if (tenantId != -1) {
+            if (tenantId > 0) {
                 [KSNetworkingManager addHeader:[NSString stringWithFormat:@"%zd", tenantId] forKey:@"coopId"];
             }
             [USER_MANAGER queryUserInfoSendLoginUMCount];

+ 148 - 18
KulexiuForTeacher/KulexiuForTeacher/Common/Base/KSAQRecordManager.m

@@ -12,9 +12,10 @@
 
 static const int kNumberBuffers = 3;
 
+
 @interface KSAQRecordManager ()
+
 @property (nonatomic, assign) SInt64 currPacket;
-@property (nonatomic, assign) AudioFileID mAudioFile;
 @property (nonatomic) dispatch_queue_t queue;
 @property (nonatomic) AudioStreamBasicDescription asBasicDesc;
 @property (nonatomic) AudioQueueRef audioQueue;
@@ -22,6 +23,8 @@ static const int kNumberBuffers = 3;
 @property (nonatomic, assign) double sampleTime;
 @property (nonatomic, assign) double sampleRate;
 
+@property (nonatomic, strong) NSMutableData *audioData;
+
 @end
 
 @implementation KSAQRecordManager
@@ -31,6 +34,7 @@ static const int kNumberBuffers = 3;
 
 - (instancetype)init {
     if (self = [super init]) {
+        [self initFilePath];
         _queue = dispatch_queue_create("ks.AQRecordManager", DISPATCH_QUEUE_SERIAL);
         [self getAudioSessionProperty];
         [self setupAudioFormat];
@@ -54,7 +58,7 @@ static const int kNumberBuffers = 3;
     //下面设置的是1 frame per packet, 所以 frame = packet
     _asBasicDesc.mBytesPerFrame = mChannelsPerFrame * 2;
     _asBasicDesc.mFramesPerPacket = 1;
-    _asBasicDesc.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsNonInterleaved | kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
+    _asBasicDesc.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
 }
 
 - (void)setupAudioQueue {
@@ -91,9 +95,6 @@ static const int kNumberBuffers = 3;
     AVAudioSession *audioSession = [AVAudioSession sharedInstance];
     _sampleTime = audioSession.IOBufferDuration;
     _sampleRate = 44100;
-    
-//    printf("_sampleTime %f \n", _sampleTime);
-//    printf("_sampleRate %f \n", _sampleRate);
 }
 
 #pragma mark ----- KSAudioSessionManagerDelegate
@@ -142,9 +143,10 @@ static const int kNumberBuffers = 3;
         NSLog(@"checkAudioAuthorization code: %d, message: %@", code, message);
     }];
     dispatch_async(_queue, ^{
-        [self initFilePath];
+        self.audioData = [NSMutableData data];
         [self enqueueBuffers];
         //start audio queue
+        
         OSStatus status = AudioQueueStart(self.audioQueue, NULL);
         if (status == noErr) {
             self.isRunning = YES;
@@ -164,6 +166,9 @@ static const int kNumberBuffers = 3;
         if (status == noErr) {
             self.isRunning = NO;
         }
+        AudioQueueDispose(self.audioQueue, true);
+        // 写入文件头
+        [self writeWAVHeader];
     });
 }
 
@@ -177,6 +182,7 @@ static const int kNumberBuffers = 3;
         printf("AudioQueuePause: %d \n", (int)status);
     });
 }
+
 - (void)resumeRecord {
     dispatch_async(_queue, ^{
         //start audio queue
@@ -203,7 +209,6 @@ static const int kNumberBuffers = 3;
                 [self freeBuffer];
             }
         }
-        AudioFileClose(self.mAudioFile);
         OSStatus status = AudioQueueDispose(self.audioQueue, true);
         if (status != noErr) { // error
             //    NSLog(@"AudioQueueDispose:%d", status);
@@ -218,16 +223,15 @@ static void inputCallback(void * inUserData,
                           const AudioTimeStamp * inStartTime,
                           UInt32 inNumberPacketDescriptions,
                           const AudioStreamPacketDescription *inPacketDescs) {
+    
     KSAQRecordManager *recorder = (__bridge KSAQRecordManager *)inUserData;
     if (recorder.isRunning == NO) {
         return;
     }
     //    printf("recorder enqueue buffer time: %f \n", inStartTime->mSampleTime / recorder.asBasicDesc.mSampleRate);
     if (inNumberPacketDescriptions > 0) {
-        // 将音频数据写入文件
-        if (AudioFileWritePackets(recorder.mAudioFile, false, inBuffer->mAudioDataByteSize, inPacketDescs, recorder.currPacket, &inNumberPacketDescriptions, inBuffer->mAudioData) == noErr) {
-            recorder.currPacket += inNumberPacketDescriptions;
-        }
+        // 保存音频数据
+        [recorder handerRecordBuffer:inBuffer withQueue:inAQ];
         //消费音频数据
         if ([recorder.delegate respondsToSelector:@selector(audioRecord:didRecordAudioData:length:)]) {
             [recorder.delegate audioRecord:recorder didRecordAudioData:inBuffer->mAudioData length:inBuffer->mAudioDataByteSize];
@@ -241,8 +245,14 @@ static void inputCallback(void * inUserData,
     }
 }
 
+
+- (void)handerRecordBuffer:(AudioQueueBufferRef)inBuffer withQueue:(AudioQueueRef)inAudioQueue {
+    NSData *data = [NSData dataWithBytes:inBuffer->mAudioData length:inBuffer->mAudioDataByteSize];
+    [self.audioData appendData:data];
+}
+
 - (void)initFilePath {
-    NSString *floderPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject] stringByAppendingPathComponent:@"AccompanyAudioData"];
+    NSString *floderPath = [self createAudioSaveFolder];
     NSFileManager *fileManager = [NSFileManager defaultManager];
     BOOL isDir = FALSE;
     BOOL isDirExist = [fileManager fileExistsAtPath:floderPath isDirectory:&isDir];
@@ -253,10 +263,6 @@ static void inputCallback(void * inUserData,
         }
         NSLog(@"创建文件夹成功,文件路径%@",floderPath);
     }
-    NSString *path = [floderPath stringByAppendingPathComponent:@"recordAudio.wav"];
-    CFURLRef url = CFURLCreateWithString(kCFAllocatorDefault, (CFStringRef)path, NULL);
-    AudioFileCreateWithURL(url, kAudioFileCAFType, &_asBasicDesc, kAudioFileFlags_EraseFile, &_mAudioFile);
-    CFRelease(url);
 }
 
 + (AUDIODEVICE_TYPE)queryAudioOutputDeviceType {
@@ -268,16 +274,140 @@ static void inputCallback(void * inUserData,
 - (NSURL *)audioUrl {
     
     if (!_audioUrl) {
-        NSString *floderPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject] stringByAppendingPathComponent:@"AccompanyAudioData"];
-        NSString *filePath = [floderPath stringByAppendingPathComponent:@"recordAudio.wav"];
+        NSString *filePath = [self findFilePath];
         _audioUrl = [NSURL fileURLWithPath:filePath];
     }
     return _audioUrl;
 }
 
+- (NSString *)findFilePath {
+    NSString *folderPath = [self createAudioSaveFolder];
+    NSString *filePath = [folderPath stringByAppendingPathComponent:@"recordAudio.wav"];
+    return filePath;
+}
+
+- (NSString *)createAudioSaveFolder {
+    NSString *folderPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject] stringByAppendingPathComponent:@"AccompanyAudioData"];
+    return folderPath;
+}
 - (void)dealloc {
     
     NSLog(@"dealloc -----KSAQRecordManager!!");
 }
 
+ 
+- (void)writeWAVHeader {
+    NSString *filePath = [self findFilePath];
+    NSFileManager *manager = [NSFileManager defaultManager];
+
+    if (![manager fileExistsAtPath:filePath]) {
+        [manager removeItemAtPath:filePath error:nil];
+    }
+    [manager createFileAtPath:filePath contents:nil attributes:nil];
+
+    NSData *recordData = self.audioData;
+    short NumChannels = 1;       //录音通道数
+    short BitsPerSample = 16;    //线性采样位数
+    int SamplingRate = 44100;     //录音采样率(Hz)
+    int numOfSamples = (int)recordData.length;
+    
+    int ByteRate = NumChannels*BitsPerSample*SamplingRate/8;
+    short BlockAlign = NumChannels*BitsPerSample/8;
+    int DataSize = NumChannels*numOfSamples*BitsPerSample/8;
+    int chunkSize = 16;
+    int totalSize = 46 + DataSize;
+    short audioFormat = 1;
+    
+    FILE *wavFile = fopen([filePath cStringUsingEncoding:NSASCIIStringEncoding], "wb");
+    if (wavFile) {
+        fwrite("RIFF", sizeof(char), 4,wavFile);
+        fwrite(&totalSize, sizeof(int), 1, wavFile);
+        fwrite("WAVE", sizeof(char), 4, wavFile);
+        fwrite("fmt ", sizeof(char), 4, wavFile);
+        fwrite(&chunkSize, sizeof(int),1,wavFile);
+        fwrite(&audioFormat, sizeof(short), 1, wavFile);
+        fwrite(&NumChannels, sizeof(short),1,wavFile);
+        fwrite(&SamplingRate, sizeof(int), 1, wavFile);
+        fwrite(&ByteRate, sizeof(int), 1, wavFile);
+        fwrite(&BlockAlign, sizeof(short), 1, wavFile);
+        fwrite(&BitsPerSample, sizeof(short), 1, wavFile);
+        fwrite("data", sizeof(char), 4, wavFile);
+        fwrite(&DataSize, sizeof(int), 1, wavFile);
+        
+        fclose(wavFile);
+        NSFileHandle *handle;
+        handle = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
+        [handle seekToEndOfFile];
+        // self.recordedData 为需要保存的pcm数据
+        [handle writeData:recordData];
+        [handle closeFile];
+    }
+}
+
+/*
+- (NSData*)GetWavheadData:(NSInteger)totalAudioLen totalDataLen:(NSInteger)totalDataLen longSampleRate:(NSInteger)longSampleRate channels:(NSInteger)channels byteRate:(NSInteger)byteRate{
+    
+    Byte header[44];
+    //4byte,资源交换文件标志:RIFF
+    header[0] = 'R';  // RIFF/WAVE header
+    header[1] = 'I';
+    header[2] = 'F';
+    header[3] = 'F';
+    //4byte,从下个地址到文件结尾的总字节数
+    header[4] = (Byte) (totalDataLen & 0xff);  //file-size (equals file-size - 8)
+    header[5] = (Byte) ((totalDataLen >> 8) & 0xff);
+    header[6] = (Byte) ((totalDataLen >> 16) & 0xff);
+    header[7] = (Byte) ((totalDataLen >> 24) & 0xff);
+    //4byte,wav文件标志:WAVE
+    header[8] = 'W';  // Mark it as type "WAVE"
+    header[9] = 'A';
+    header[10] = 'V';
+    header[11] = 'E';
+    //4byte,波形文件标志:FMT(最后一位空格符)
+    header[12] = 'f';  // Mark the format section 'fmt ' chunk
+    header[13] = 'm';
+    header[14] = 't';
+    header[15] = ' ';
+    //4byte,音频属性
+    header[16] = 16;   // 4 bytes: size of 'fmt ' chunk, Length of format data.  Always 16
+    header[17] = 0;
+    header[18] = 0;
+    header[19] = 0;
+    //2byte,格式种类(1-线性pcm-WAVE_FORMAT_PCM,WAVEFORMAT_ADPCM)
+    header[20] = 1;  // format = 1 ,Wave type PCM
+    header[21] = 0;
+    //2byte,通道数
+    header[22] = (Byte) channels;  // channels
+    header[23] = 0;
+    //4byte,采样率
+    header[24] = (Byte) (longSampleRate & 0xff);
+    header[25] = (Byte) ((longSampleRate >> 8) & 0xff);
+    header[26] = (Byte) ((longSampleRate >> 16) & 0xff);
+    header[27] = (Byte) ((longSampleRate >> 24) & 0xff);
+    //4byte 传输速率,Byte率=采样频率*音频通道数*每次采样得到的样本位数/8,00005622H,也就是22050Byte/s=11025*1*16/8。
+    header[28] = (Byte) (byteRate & 0xff);
+    header[29] = (Byte) ((byteRate >> 8) & 0xff);
+    header[30] = (Byte) ((byteRate >> 16) & 0xff);
+    header[31] = (Byte) ((byteRate >> 24) & 0xff);
+    //2byte   一个采样多声道数据块大小,块对齐=通道数*每次采样得到的样本位数/8,0002H,也就是2=1*16/8
+    header[32] = (Byte) (channels * 16 / 8);
+    header[33] = 0;
+    //2byte,采样精度-PCM位宽
+    header[34] = 16; // bits per sample
+    header[35] = 0;
+    //4byte,数据标志:data
+    header[36] = 'd'; //"data" marker
+    header[37] = 'a';
+    header[38] = 't';
+    header[39] = 'a';
+    //4byte,从下个地址到文件结尾的总字节数,即除了wav header以外的pcm data length(纯音频数据)
+    header[40] = (Byte) (totalAudioLen & 0xff);  //data-size (equals file-size - 44).
+    header[41] = (Byte) ((totalAudioLen >> 8) & 0xff);
+    header[42] = (Byte) ((totalAudioLen >> 16) & 0xff);
+    header[43] = (Byte) ((totalAudioLen >> 24) & 0xff);
+    
+    return [[NSData alloc] initWithBytes:header length:44];;
+}
+*/
+
 @end

+ 24 - 19
KulexiuForTeacher/KulexiuForTeacher/Common/Base/KSAccompanyWebViewController.m

@@ -118,6 +118,8 @@
 
 @property (nonatomic, strong) NSString *accompanyUrl;
 
+@property (nonatomic, assign) BOOL muteAccompany; // 是否静音
+
 @end
 
 @implementation KSAccompanyWebViewController
@@ -491,10 +493,17 @@
                  self.evaluatParm = nil;
                  [self stopRecordService];
                  [self postMessage:parm];
-//                 [self sendEndMessage];
                 [self stopMp3Player]; // 停止播放
             }
             else if ([[parm ks_stringValueForKey:@"api"] isEqualToString:@"startRecording"]) { //  开始录制
+                NSDictionary *content = [parm ks_dictionaryValueForKey:@"content"];
+                if ([[content allKeys] containsObject:@"accompanimentState"]) {
+                    BOOL mute = [content ks_boolValueForKey:@"accompanimentState"] == NO;
+                    self.muteAccompany = mute;
+                }
+                else {
+                    self.muteAccompany = NO;
+                }
                 if (self->_videoRecordManager) {
                     [self.videoRecordManager clearVideoFile];
                 }
@@ -924,7 +933,9 @@
                         mergeView.songName = [content ks_stringValueForKey:@"title"];
                         mergeView.coverImage = [content ks_stringValueForKey:@"coverImg"];
                         MJWeakSelf;
-                        [mergeView configWithVideoUrl:self.videoRecordManager.videoFileURL bgAudioUrl:self.bgAudioUrl remoteBgUrl:self.accompanyUrl  recordUrl:self.recordUrl offsetTime:self.offsetTime mergeCallback:^{
+                        NSInteger micDelay = [UserDefaultObjectForKey(@"micDelay") integerValue];
+                        NSInteger defaultDelay = self.offsetTime + micDelay;
+                        [mergeView configWithVideoUrl:self.videoRecordManager.videoFileURL bgAudioUrl:self.bgAudioUrl remoteBgUrl:self.accompanyUrl  recordUrl:self.recordUrl offsetTime:defaultDelay mergeCallback:^{
                             [weakSelf musicPublishCallBack:content];
                         }];
                     }
@@ -1234,6 +1245,7 @@
           NSLog(@"---- compare - record did start %f", time);
           // 播放伴奏
           dispatch_main_sync_safe(^{
+ self.musicPlayer.isMute = self.muteAccompany;
                [self.musicPlayer startPlay];
           });
      }
@@ -1247,27 +1259,20 @@
     }
     NSData *pushData = [[NSData alloc] initWithBytes:data length:length];
     if (self.isCompareStart) { // 发送评测开始消息
-        dispatch_async(dispatch_get_main_queue(), ^{
-            NSDate *date = [NSDate date];
-            NSTimeInterval inteveral = [date timeIntervalSince1970];
-            double beginTime = inteveral - audioRecord.sampleTime;
-            NSDictionary *parm = @{
-                @"api" : @"recordStartTime",
-                @"content" : @{@"inteveral" : [NSNumber numberWithDouble:beginTime]}
-            };
-            [self postMessage:parm];
-        });
+        NSDate *date = [NSDate date];
+        NSTimeInterval inteveral = [date timeIntervalSince1970];
+        double beginTime = inteveral - audioRecord.sampleTime;
+        NSDictionary *parm = @{
+            @"api" : @"recordStartTime",
+            @"content" : @{@"inteveral" : [NSNumber numberWithDouble:beginTime]}
+        };
+        [self postMessage:parm];
         
         NSLog(@"--------- send start message");
         _isCompareStart = NO;
         NSString *startMessage = @"recordStart";
         NSString *startString = [self configDataCommond:startMessage body:nil type:@"SOUND_COMPARE"];
         [self sendDataToSocketService:startString];
-         
-//         // 发送开始录制的消息给H5
-//         if (self.recordParm) {
-//              [self postMessage:self.recordParm];
-//         }
     }
     else if (self.isSoundCheckStart) { // 校音开始
         
@@ -1292,7 +1297,7 @@
         NSString *startString = [self configDataCommond:checkStartMessage body:parm type:@"DELAY_CHECK"];
         [self sendDataToSocketService:startString];
     }
-//        NSLog(@"--------- send audio data length %d", length);
+    //        NSLog(@"--------- send audio data length %d", length);
     [self sendDataToSocketService:pushData];
 }
 
@@ -1669,7 +1674,7 @@
     }
 }
 
-- (void)preparePlay:(kSNewPlayer *)player {
+- (void)playerIsReadyPlay:(kSNewPlayer *)player {
     if (player == self.musicPlayer) {
         self.musicPlayerReady = YES;
     }

+ 17 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/Base/KSTabBarViewController.m

@@ -10,6 +10,7 @@
 #import "KSBaseViewController.h"
 #import "UIImage+Color.h"
 #import "AnimationHelper.h"
+#import "MineWorksViewController.h"
 
 @interface KSTabBarViewController ()<UITabBarControllerDelegate>
 
@@ -21,9 +22,21 @@
     [super viewDidLoad];
     // Do any additional setup after loading the view.
     self.delegate = self;
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showDraftPage) name:@"DisplayDraft" object:nil];
+
     [self configItems];
 }
 
+- (void)showDraftPage {
+    [self tabBarSelectedWithIndex:4];
+    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+        CustomNavViewController *navCtrl = self.selectedViewController;
+        MineWorksViewController *ctrl = [[MineWorksViewController alloc] init];
+        [ctrl displayDraft:YES];
+        [navCtrl pushViewController:ctrl animated:YES];
+    });
+}
+
 - (void)configItems {
     NSArray *controllerArray = @[@"HomeViewController",@"CourseViewController",@"ChatViewController",@"ShopMallViewController",@"MineViewController"];
     NSArray *titleArray = @[@"首页",@"课表",@"聊天",@"商城",@"我的"];
@@ -146,6 +159,10 @@
     lastClickTime = 0;
     return YES;
 }
+
+- (void)dealloc {
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
 /*
 #pragma mark - Navigation
 

+ 189 - 31
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/KSMediaMergeView.m

@@ -20,6 +20,8 @@
 #import "KSMediaManager.h"
 #import <RSKImageCropper/RSKImageCropper.h>
 #import "KSVideoCropViewController.h"
+#import "KSAudioSessionManager.h"
+
 
 @interface KSMediaMergeView ()<kSNewPlayerManagerDelegate,KSVideoPlayerViewDelegate,RSKImageCropViewControllerDelegate,RSKImageCropViewControllerDataSource>
 
@@ -102,6 +104,20 @@
 
 @property (nonatomic, strong) NSString *videoCoverUrl; // 视频封面
 
+@property (nonatomic, assign) BOOL isOtherLogin;
+
+@property (nonatomic, assign) NSInteger defaultDelay;
+
+@property (nonatomic, assign) CGFloat bgPlayerRate;
+
+@property (nonatomic, assign) NSInteger addTime;
+
+//@property (nonatomic, strong) UILabel *offsetTimeLabel;
+
+@property (nonatomic, assign) NSInteger evaluateDelay;
+
+@property (nonatomic, assign) BOOL fromDraftPage; // 是否从草稿页面进入
+
 @end
 
 @implementation KSMediaMergeView
@@ -110,17 +126,24 @@
     self = [super init];
     if (self) {
         self.hasModify = NO;
+        self.bgPlayerRate = 1.0;
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appEnterBackground) name:@"appEnterBackground" object:nil];
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(otherLogin) name:@"otherLogin" object:nil];
+        self.addTime = 0;
+        [self configAudioSession];
+
     }
     return self;
 }
 
+- (void)configAudioSession {
+    [[[KSAudioSessionManager alloc] init] configAudioSession:AUDIOCONFIG_PLAYANDRECORD];
+}
+
 - (void)otherLogin {
     [self stopPlay];
 }
 
-
 - (void)appEnterBackground {
     [self stopPlay];
 }
@@ -142,13 +165,13 @@
     else {
         self.isVideoPlay = NO;
     }
+    self.fromDraftPage = NO;
     self.remoteBgAudioUrl = remoteBgUrl;
     self.videoUrl = videoUrl;
     self.bgAudioUrl = bgAudioUrl;
     self.recordUrl = recordUrl;
-    if (offsetTime > 300) {
-        offsetTime = 300;
-    }
+    self.defaultDelay = -offsetTime;  // default delay = evaluate delay
+    self.evaluateDelay = -offsetTime;  // 播放延迟和收音延迟
     self.originalOffset = 0;
     [self.contrlView configWithOffsetTime:0];
     self.contrlView.hideBackView = NO;
@@ -157,6 +180,7 @@
 }
 
 - (void)configRemoteVideoUrl:(NSString *)remoteVideoUrl bgAudioUrl:(NSString *)remoteBgAudioUrl recordUrl:(NSString *)remoteRecrodUrl jsonConfig:(NSString *)jsonConfig callback:(DraftEditCallback)callback {
+    self.addTime = 0;
     if (callback) {
         self.draftCallback = callback;
     }
@@ -166,12 +190,15 @@
     else {
         self.isVideoPlay = NO;
     }
+    self.fromDraftPage = YES;
     self.remoteVideoUrl = remoteVideoUrl;
     self.remoteBgAudioUrl = remoteBgAudioUrl;
     self.remoteRecrodUrl = remoteRecrodUrl;
     
     self.preJsonDic = [jsonConfig mj_JSONObject];
     self.originalOffset = [self.preJsonDic ks_integerValueForKey:@"offset"];
+    self.defaultDelay = [self.preJsonDic ks_integerValueForKey:@"defaultDelay"];
+    self.evaluateDelay = [self.preJsonDic ks_integerValueForKey:@"evaluateDelay"];  // 播放延迟和收音延迟
     self.offsetTime = self.originalOffset;
     [self.contrlView configWithOffsetTime:self.originalOffset];
     self.contrlView.hideBackView = YES;
@@ -323,7 +350,7 @@
             make.top.mas_equalTo(self.animationView.mas_top).offset(86);
         }];
         [self.animationView configWithImageWithUrl:self.coverImage];
-
+        
     }
     
     [self addSubview:self.playControlView];
@@ -359,7 +386,19 @@
         make.height.mas_equalTo(22);
         make.right.mas_greaterThanOrEqualTo(self.contrlView.mas_left).offset(20);
     }];
-    [self startPlay];
+    
+//    self.offsetTimeLabel = [[UILabel alloc] init];
+//    self.offsetTimeLabel.font = [UIFont systemFontOfSize:14.0f weight:UIFontWeightSemibold];
+//    self.offsetTimeLabel.textColor = self.isVideoPlay ? HexRGB(0xffffff) : HexRGB(0x131415);
+//    [self addSubview:self.offsetTimeLabel];
+//    [self.offsetTimeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
+//        make.height.mas_equalTo(22);
+//        make.centerX.mas_equalTo(self.mas_centerX);
+//        make.centerY.mas_equalTo(songLabel.mas_centerY);
+//    }];
+    
+//    [self startPlay];
+    // 缓冲完成后播放
 }
 
 - (void)backAction {
@@ -409,21 +448,27 @@
 }
 
 - (void)startPlay {
+    self.bgPlayerRate = 1.0;
     self.playControlView.isPlay = YES;
     self.animationView.isPlay = YES;
     NSInteger recordPlayerPosition = self.playControlView.playScheduleTime*1000;
-    NSLog(@"--recordPlayer start---");
     [self.recordPlayer seekToTimePlay:recordPlayerPosition];
-    NSInteger realOffsetTime = self.offsetTime;
+    
+    NSInteger realOffsetTime = self.offsetTime + self.evaluateDelay;
     NSInteger offsetTime = recordPlayerPosition + realOffsetTime;
-    if (offsetTime > 0) {
-        [self.bgPlayer seekToTimePlay:labs(offsetTime)];
+    if (offsetTime >= 0) {
+        [self.bgPlayer seekToTimePlay:offsetTime];
     }
     else {
-        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(labs(offsetTime) * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
-            NSLog(@"--bgPlayer start---");
-
-            [self.bgPlayer startPlay];
+        [self.bgPlayer seekToStart];
+        @weakObj(self);
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((labs(offsetTime) + self.addTime) * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
+            @strongObj(self);
+//            NSLog(@"--bgPlayer start---");
+            if (self.playControlView.isPlay == YES) {
+                [self.bgPlayer resumePlay];
+                [self snapPlayerProgress];
+            }
         });
     }
     
@@ -465,24 +510,29 @@
         case PLAYERTYPE_RATE:
         {
             [self.recordPlayer seekOffsetTime:rate*1000];
-            NSInteger realOffsetTime = self.offsetTime;
-
+            
+            NSInteger realOffsetTime = self.offsetTime + self.evaluateDelay;
             NSInteger offsetTime = rate*1000 + realOffsetTime;
-            if (offsetTime > 0) {
-                [self.bgPlayer seekOffsetTime:labs(offsetTime)];
+            if (offsetTime >= 0) {
+                [self.bgPlayer seekOffsetTime:offsetTime];
             }
             else {
                 [self.bgPlayer puasePlay];
                 if (self.recordPlayer.isPlaying) {
-                    
-                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(labs(offsetTime) * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
-                        [self.bgPlayer startPlay];
+                    [self.bgPlayer seekToStart];
+                    @weakObj(self);
+                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((labs(offsetTime) + self.addTime) * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
+                        @strongObj(self);
+                        if (self.playControlView.isPlay == YES) {
+                            [self.bgPlayer resumePlay];
+                            [self snapPlayerProgress];
+                        }
+                        
                     });
                 }
                 else {
                     [self.bgPlayer seekToStart];
                 }
-                
             }
             
             if (self.isVideoPlay) {
@@ -495,6 +545,21 @@
     }
 }
 
+// 对齐播放延迟
+- (void)snapPlayerProgress {
+    @weakObj(self);
+    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
+        @strongObj(self);
+        if (self.playControlView.isPlay == YES) {
+            CMTime time = [self.recordPlayer getCurrentPlayTime];
+            NSInteger newDelayTime = CMTimeGetSeconds(time) * 1000 + self.offsetTime + self.evaluateDelay;
+            [self.recordPlayer seekOffsetTimeNoPuase:CMTimeGetSeconds(time) * 1000];
+            [self.bgPlayer seekOffsetTime:newDelayTime];
+        }
+        
+    });
+}
+
 - (void)startTimer {
     [self.timer setFireDate:[NSDate distantPast]];
 }
@@ -537,7 +602,8 @@
             self.hasModify = YES;
             if (self.bgPlayer.isPlaying) {
                 CMTime time = [self.recordPlayer getCurrentPlayTime];
-                NSInteger newDelayTime = CMTimeGetSeconds(time) * 1000 + offsetTime;
+                NSInteger newDelayTime = CMTimeGetSeconds(time) * 1000 + offsetTime + self.evaluateDelay;
+                [self.recordPlayer seekOffsetTime:CMTimeGetSeconds(time) * 1000];
                 [self.bgPlayer seekOffsetTime:newDelayTime];
             }
             
@@ -546,6 +612,8 @@
             break;
         case MERGEACTION_SAVE: // 保存
         {
+            // 暂停播放
+            [self stopPlay];
             [self saveCurrentDraft:NO];
         }
             break;
@@ -596,6 +664,7 @@
             // 暂停播放
             [self stopPlay];
             self.isChooseVideoCover = NO;
+
             // 调用相册
             self.mediaManager = [[KSMediaManager alloc] init];
             self.mediaManager.mediaType = MEDIATYPE_PHOTO;
@@ -688,7 +757,6 @@
 - (void)uploadMusicCover {
     NSString *tips = self.isVideoPlay ? @"视频合成中" : @"音频合成中";
     [LOADING_MANAGER showProgressLoading:tips progress:0];
-
     if (self.settingImage) {
         NSData *imgData = [UIImage turnsImaegDataByImage:self.settingImage];
         NSString *fileName = @"musicCoverImg";
@@ -744,8 +812,8 @@
     CGFloat rate = 1.0/3;
     if (self.isVideoPlay) {
         [LOADING_MANAGER showProgressLoading:@"视频合成中" progress:0];
-        
-        [KSMediaEditor mixVideoWithRecordAudio:self.recordUrl recordVolume:self.recordPlayer.volume bgAudio:self.bgAudioUrl bgAudioVolume:self.bgPlayer.volume offsetTime:self.offsetTime videoUrlStr:self.videoUrl completion:^(NSString * _Nonnull outPath, BOOL isSuccess, NSString * _Nonnull desc) {
+        CGFloat offsetTime = self.offsetTime + self.evaluateDelay;
+        [KSMediaEditor mixVideoWithRecordAudio:self.recordUrl recordVolume:self.recordPlayer.volume bgAudio:self.bgAudioUrl bgAudioVolume:self.bgPlayer.volume offsetTime:offsetTime videoUrlStr:self.videoUrl completion:^(NSString * _Nonnull outPath, BOOL isSuccess, NSString * _Nonnull desc) {
             [LOADING_MANAGER showProgressLoading:@"视频合成中" progress:progress];
             // 保存文件到指定文件夹
             if (isSuccess) {
@@ -766,7 +834,7 @@
     }
     else {
         [LOADING_MANAGER showProgressLoading:@"音频合成中" progress:0];
-        NSInteger realOffsetTime = self.offsetTime;
+        NSInteger realOffsetTime = self.offsetTime + self.evaluateDelay;
         [KSMediaEditor mixRecordAudio:self.recordUrl recordVolume:self.recordPlayer.volume bgAudio:self.bgAudioUrl bgAudioVolume:self.bgPlayer.volume offsetTime:realOffsetTime completion:^(NSString * _Nonnull outPath, BOOL isSuccess, NSString * _Nonnull desc) {
             // 保存文件到指定文件夹
             [LOADING_MANAGER showProgressLoading:@"音频合成中" progress:progress];
@@ -885,24 +953,85 @@
 
 
 #pragma mark ----- player delegate
+
+- (void)videoPlayerIsReadyPlay:(AVPlayer *)player {
+    
+}
+
+- (void)playerIsReadyPlay:(kSNewPlayer *)player {
+    if (self.bgPlayer.isReady && self.recordPlayer.isReady) {
+        [self startPlay];
+    }
+}
+
+
 - (void)getSongCurrentTime:(NSInteger)currentTime andTotalTime:(NSInteger)totalTime andProgress:(CGFloat)progress currentInterval:(NSTimeInterval)currentInterval playTime:(NSTimeInterval)playTime inPlayer:(kSNewPlayer *_Nonnull)player {
     if (player == self.recordPlayer) {
 //        NSLog(@"--- ----  ----- %f", playTime);
         self.playControlView.playScheduleTime = (NSInteger)(playTime / 1000);
+        NSInteger realOffset = (NSInteger)(CMTimeGetSeconds([self.bgPlayer getCurrentPlayTime]) *1000 - CMTimeGetSeconds([self.recordPlayer getCurrentPlayTime])*1000);
+//        NSLog(@" offset ---- %ld" , realOffset);
+        //  如果延迟大于11ms 调整
+        NSInteger expectOffset = self.offsetTime + self.evaluateDelay;
+        NSLog(@"---- expectOffset == %@", [NSString stringWithFormat:@"实际: %ld, 期望偏差 %ld", realOffset, expectOffset]);
+//        self.offsetTimeLabel.text = [NSString stringWithFormat:@"实际: %ld, 期望偏差 %ld", realOffset, expectOffset];
+//        if (labs(labs(realOffset) - labs(expectOffset)) >= 11) { // 需要调整
+//
+//            if (labs(realOffset) < labs(expectOffset)) { // 要扩大差距
+//                if (expectOffset > 0) {  // 300
+//                    if (self.bgPlayerRate != 1.02) {
+//                        self.bgPlayerRate = 1.02;
+//                        [self.bgPlayer configPlayerRate:1.02];
+//                        [self.recordPlayer configPlayerRate:0.98];
+//                    }
+//                }
+//                else { // expectOffset < 0 -300
+//                    if (self.bgPlayerRate != 0.98) {
+//                        self.bgPlayerRate = 0.98;
+//                        [self.bgPlayer configPlayerRate:0.98];
+//                        [self.recordPlayer configPlayerRate:1.02];
+//                    }
+//                }
+//            }
+//            else { // 需要减小差距
+//                if (realOffset > 0) {
+//                    if (self.bgPlayerRate != 0.98) {
+//                        self.bgPlayerRate = 0.98;
+//                        [self.bgPlayer configPlayerRate:0.98];
+//                        [self.recordPlayer configPlayerRate:1.02];
+//                    }
+//                }
+//                else {
+//                    if (self.bgPlayerRate != 1.02) {
+//                        self.bgPlayerRate = 1.02;
+//                        [self.bgPlayer configPlayerRate:1.02];
+//                        [self.recordPlayer configPlayerRate:0.98];
+//                    }
+//                }
+//            }
+//        }
+//        else {
+//            if (self.bgPlayerRate != 1.0) {
+//                self.bgPlayerRate = 1.0;
+//                [self.recordPlayer configPlayerRate:1.0];
+//                [self.bgPlayer configPlayerRate:1.0];
+//            }
+//        }
     }
     else {
-//        NSLog(@"--- ---- bgPlayer   ----- %f", playTime);
+//        NSLog(@"------- bgPlayer   ----- %f", playTime);
     }
 }
 
 - (void)playFinished:(kSNewPlayer *)player {
     if (player == self.recordPlayer) {
+        
         [self.recordPlayer puasePlay];
         [self.recordPlayer seekToStart];
         
         [self.bgPlayer puasePlay];
         [self.bgPlayer seekToStart];
-        self.isPause = NO;
+        
         self.animationView.isPlay = NO;
         self.playControlView.isPlay = NO;
         [self.playAnimationView stopAnimation];
@@ -970,6 +1099,9 @@
     [parm setValue:@(self.offsetTime) forKey:@"offset"];
     [parm setValue:@(self.originalVolume) forKey:@"originalVolume"];
     [parm setValue:@(self.accompanyVolume) forKey:@"accompanyVolume"];
+    [parm setValue:@(self.defaultDelay) forKey:@"defaultDelay"];
+    [parm setValue:@(self.evaluateDelay) forKey:@"evaluateDelay"];
+
     self.jsonConfig = [parm mj_JSONString];
     [KSNetworkingManager saveMusicMessage:KS_POST jsonConfig:self.jsonConfig img:self.coverImage videoUrl:fileUrl accompanyUrl:self.remoteBgAudioUrl desc:self.desc type:type musicPracticeRecordId:self.recordId videoImg:self.videoCoverUrl success:^(NSDictionary * _Nonnull dic) {
         if ([dic ks_integerValueForKey:@"code"] == 200) {
@@ -984,6 +1116,11 @@
                     if (needBack) {
                         [self removeViewTips:NO];
                     }
+                    else { // 从云教练进入 点击保存 显示保存弹窗
+                        if (self.fromDraftPage == NO) {
+                            [self showSaveDraftTipsAlert];
+                        }
+                    }
                 }];
             }
         }
@@ -996,6 +1133,27 @@
     }];
 }
 
+- (void)showSaveDraftTipsAlert {
+    MJWeakSelf;
+    [self.alertView configTitle:@"提示" descMessage:@"已成功保存到草稿,草稿7天未发布将自动清理。" leftButtonTitle:@"确认" rightButtonTitle:@"查看草稿" leftButtonAction:^{
+        
+    } rightButtonAction:^{
+        [weakSelf displayDraft];
+    }];
+    [self.alertView showAlert];
+}
+
+- (void)displayDraft {
+    
+    // 跳转到我的作品
+    UIViewController *baseCtrl = [self findViewController];
+    [baseCtrl.navigationController popToRootViewControllerAnimated:YES];
+    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+        [[NSNotificationCenter defaultCenter] postNotificationName:@"DisplayDraft" object:nil];
+    });
+    
+}
+
 - (void)sendVideoActionWithUrlPath:(NSURL *)fileUrl isFormal:(BOOL)isFormal beginProgress:(CGFloat)beginProgress rate:(CGFloat)rate success:(void (^)(NSString * _Nonnull uploadVideoUrl))success failure:(void (^)(NSString * _Nonnull desc))faliure {
     NSString *tips = isFormal ? @"正在上传作品" : @"正在上传草稿";
     [LOADING_MANAGER showProgressLoading:tips progress:beginProgress];
@@ -1034,8 +1192,7 @@
     [LOADING_MANAGER showProgressLoading:tips progress:beginProgress];
     NSData *fileData = [NSData dataWithContentsOfURL:fileUrl];
     NSString *suffix = [NSString stringWithFormat:@".%@",[fileUrl pathExtension]];
-    [[KSUploadManager shareInstance] configBucketName:@"klx"];
-    [UPLOAD_MANAGER uploadFile:fileData fileName:@"evaluateAudio" fileSuffix:suffix progress:^(int64_t bytesWritten, int64_t totalBytes) {
+    [[KSUploadManager shareInstance] configBucketName:@"klx"];    [UPLOAD_MANAGER uploadFile:fileData fileName:@"evaluateAudio" fileSuffix:suffix progress:^(int64_t bytesWritten, int64_t totalBytes) {
         // 显示进度
         float progress = (bytesWritten*1.0 / totalBytes) * rate + beginProgress;
         dispatch_main_async_safe(^{
@@ -1197,6 +1354,7 @@
     
     [controller dismissViewControllerAnimated:YES completion:nil];
 }
+
 /*
 // Only override drawRect: if you perform custom drawing.
 // An empty implementation adversely affects performance during animation.

+ 23 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/MusicPlayer/AVPlayer+KSSeekSmoothly.h

@@ -0,0 +1,23 @@
+//
+//  AVPlayer+KSSeekSmoothly.h
+//  KulexiuSchoolStudent
+//
+//  Created by 王智 on 2023/11/24.
+//
+
+#import <AVFoundation/AVFoundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface AVPlayer (KSSeekSmoothly)
+
+- (void)ss_seekToTime:(CMTime)time;
+
+- (void)ss_seekToTime:(CMTime)time
+      toleranceBefore:(CMTime)toleranceBefore
+       toleranceAfter:(CMTime)toleranceAfter
+    completionHandler:(void (^)(BOOL))completionHandler;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 97 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/MusicPlayer/AVPlayer+KSSeekSmoothly.m

@@ -0,0 +1,97 @@
+//
+//  AVPlayer+KSSeekSmoothly.m
+//  KulexiuSchoolStudent
+//
+//  Created by 王智 on 2023/11/24.
+//
+
+#import "AVPlayer+KSSeekSmoothly.h"
+#import <objc/runtime.h>
+
+@interface AVPlayerSeeker : NSObject
+{
+    CMTime targetTime;
+    BOOL isSeeking;
+}
+
+@property (weak, nonatomic) AVPlayer *player;
+
+@property (nonatomic, strong) dispatch_queue_t queue;
+
+@end
+
+@implementation AVPlayerSeeker
+
+- (instancetype)initWithPlayer:(AVPlayer *)player {
+    self = [super init];
+    if (self) {
+        self.player = player;
+        _queue = dispatch_queue_create("ks.seekTimeQueue", DISPATCH_QUEUE_SERIAL);
+    }
+    return self;
+}
+
+- (void)seekSmoothlyToTime:(CMTime)time toleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter completionHandler:(void (^)(BOOL))completionHandler {
+    targetTime = time;
+    if (!isSeeking) {
+        [self trySeekToTargetTimeWithToleranceBefore:toleranceBefore toleranceAfter:toleranceAfter completionHandler:completionHandler];
+    }
+}
+
+- (void)trySeekToTargetTimeWithToleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter completionHandler:(void (^)(BOOL))completionHandler {
+    if (self.player.currentItem.status == AVPlayerItemStatusReadyToPlay) {
+        [self seekToTargetTimeToleranceBefore:toleranceBefore toleranceAfter:toleranceAfter completionHandler:completionHandler];
+    }
+}
+
+- (void)seekToTargetTimeToleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter completionHandler:(void (^)(BOOL))completionHandler {
+    isSeeking = YES;
+    CMTime seekingTime = targetTime;
+    
+    dispatch_async(_queue, ^{
+        [self.player seekToTime:seekingTime toleranceBefore:toleranceBefore
+                 toleranceAfter:toleranceAfter completionHandler:
+         ^(BOOL isFinished) {
+            if (CMTIME_COMPARE_INLINE(seekingTime, ==, self->targetTime)) {
+                 // seek completed
+                 self->isSeeking = NO;
+                dispatch_main_async_safe(^{
+                    if (completionHandler) {
+                        completionHandler(isFinished);
+                    }
+                });
+             } else {
+                 // targetTime has changed, seek again
+                 [self trySeekToTargetTimeWithToleranceBefore:toleranceBefore toleranceAfter:toleranceAfter completionHandler:completionHandler];
+             }
+         }];
+    });
+    
+    
+}
+
+@end
+
+static NSString *seekerKey = @"ss_seeker";
+
+@implementation AVPlayer (KSSeekSmoothly)
+
+- (void)ss_seekToTime:(CMTime)time {
+    [self ss_seekToTime:time toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:nil];
+}
+
+- (void)ss_seekToTime:(CMTime)time toleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter completionHandler:(void (^)(BOOL))completionHandler {
+
+    AVPlayerSeeker *seeker = objc_getAssociatedObject(self, &seekerKey);
+    if (!seeker) {
+        seeker = [[AVPlayerSeeker alloc] initWithPlayer:self];
+        objc_setAssociatedObject(self, &seekerKey, seeker, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+    }
+
+//    [self pause];
+    [seeker seekSmoothlyToTime:time toleranceBefore:toleranceBefore toleranceAfter:toleranceAfter completionHandler:completionHandler];
+}
+
+
+
+@end

+ 14 - 1
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/MusicPlayer/kSNewPlayer.h

@@ -9,6 +9,7 @@
 #import <UIKit/UIKit.h>
 #import <AVFoundation/AVFoundation.h>
 
+
 @class kSNewPlayer;
 
 @protocol kSNewPlayerManagerDelegate <NSObject>
@@ -21,12 +22,16 @@
 
 - (void)playFinished:(kSNewPlayer *_Nonnull)player;
 
+- (void)playerIsReadyPlay:(kSNewPlayer *_Nonnull)player;
+
 @end
 
 NS_ASSUME_NONNULL_BEGIN
 
 @interface kSNewPlayer : NSObject
 
+@property (nonatomic, assign) BOOL isReady;
+
 @property (nonatomic, assign) BOOL isPlaying;
 
 @property (nonatomic, assign) BOOL isMute;
@@ -40,8 +45,11 @@ NS_ASSUME_NONNULL_BEGIN
  */
 @property(nonatomic,weak)id<kSNewPlayerManagerDelegate>delegate;
 
+
 + (instancetype)shareInstance;
 
+- (void)configPlayerRate:(CGFloat)rate;
+
 // 播放线上文件
 - (void)preparePlaySongWithUrl:(NSString *)urlStr;
 
@@ -52,6 +60,8 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (void)startPlay;
 
+- (void)startPlayNoSeek;
+
 - (void)resumePlay;
 
 - (void)puasePlay;
@@ -62,9 +72,12 @@ NS_ASSUME_NONNULL_BEGIN
 - (void)seekToStart;
 // 从某个位置开始播放 ms
 - (void)seekToTimePlay:(NSInteger)time;
-
+// 调整进度到某个位置
+- (void)seekToTime:(NSInteger)time callback:(void(^)(void))callback;
 // 调整进度
 - (void)seekOffsetTime:(NSInteger)offsetTime;
+// 调整进度
+- (void)seekOffsetTimeNoPuase:(NSInteger)offsetTime;
 
 - (CMTime)getCurrentPlayTime;
 

+ 92 - 28
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/MusicPlayer/kSNewPlayer.m

@@ -8,19 +8,24 @@
 
 #import "kSNewPlayer.h"
 #import "UrlDecode.h"
+#import "AVPlayer+KSSeekSmoothly.h"
 
 @interface kSNewPlayer ()
 
-@property(nonatomic,strong) AVPlayerItem *songItem;
-
 @property(nonatomic,strong) AVPlayer *player;
 
+@property(nonatomic,strong) AVPlayerItem *songItem;
+
 @property(nonatomic,retain)id timeObserver;//时间观察
 
 @property (nonatomic, assign) BOOL cacheFinish;
 
 @property (nonatomic, assign) BOOL hasFreeObserver;
 
+@property (nonatomic, assign) BOOL isSeekInProgress;
+
+@property (nonatomic, assign) CMTime chaseTime;
+
 @end
 
 @implementation kSNewPlayer
@@ -52,11 +57,15 @@
     }
     _songItem = [[AVPlayerItem alloc] initWithAsset:asset];
     _player = [AVPlayer playerWithPlayerItem:_songItem];
+    if ([_player respondsToSelector:@selector(automaticallyWaitsToMinimizeStalling)]) {
+        _player.automaticallyWaitsToMinimizeStalling = NO;
+    }
+
     //添加监听
     [self addNotificationAndObserver];
     self.volume = 1.0;
     self.isMute = NO;
-//    AVAudioPlayer *player = [[AVAudioPlayer alloc] init];
+    //    AVAudioPlayer *player = [[AVAudioPlayer alloc] init];
 }
 
 
@@ -71,6 +80,9 @@
     }
     _songItem = [AVPlayerItem playerItemWithAsset:asset];
     _player = [AVPlayer playerWithPlayerItem:_songItem];
+    if ([_player respondsToSelector:@selector(automaticallyWaitsToMinimizeStalling)]) {
+        _player.automaticallyWaitsToMinimizeStalling = NO;
+    }
     //添加监听
     [self addNotificationAndObserver];
     self.volume = 1.0;
@@ -86,6 +98,9 @@
     }
     _songItem = [AVPlayerItem playerItemWithAsset:asset];
     _player = [AVPlayer playerWithPlayerItem:_songItem];
+    if ([_player respondsToSelector:@selector(automaticallyWaitsToMinimizeStalling)]) {
+        _player.automaticallyWaitsToMinimizeStalling = NO;
+    }
     //添加监听
     [self addNotificationAndObserver];
     self.volume = 1.0;
@@ -106,19 +121,35 @@
         [self addPlayToEndObserver];
     }
 }
-
+- (void)configPlayerRate:(CGFloat)rate {
+    if (_isPlaying) {
+        if (rate >= 0) {
+            self.player.rate = rate;
+        }
+    }
+    
+}
 - (void)startPlay {
     if (_isPlaying) {
         [_player pause];
     }
     _isPlaying = YES;
-    MJWeakSelf;
+    @weakObj(self);
     CMTime toleranceTime = CMTimeMake(1, 1000);
-    [_player seekToTime:kCMTimeZero toleranceBefore:toleranceTime toleranceAfter:toleranceTime completionHandler:^(BOOL finished) {
-        [weakSelf.player play];
+    [self.player ss_seekToTime:kCMTimeZero toleranceBefore:toleranceTime toleranceAfter:toleranceTime completionHandler:^(BOOL finished) {
+        @strongObj(self);
+        [self.player play];
     }];
 }
 
+- (void)startPlayNoSeek {
+    if (_isPlaying) {
+        [_player pause];
+    }
+    _isPlaying = YES;
+    [self.player play];
+}
+
 - (void)resumePlay {
     if (_isPlaying) {
         [_player pause];
@@ -140,34 +171,58 @@
         [_player pause];
     }
     [self removeAllNoticeAndObserver];
+    [self resetPlayer];
 }
 
 - (void)seekToStart {
     CMTime toleranceTime = CMTimeMake(1, 1000);
-    [_player seekToTime:kCMTimeZero toleranceBefore:toleranceTime toleranceAfter:toleranceTime completionHandler:^(BOOL finished) {
+    [self.player ss_seekToTime:kCMTimeZero toleranceBefore:toleranceTime toleranceAfter:toleranceTime completionHandler:^(BOOL finished) {
         
     }];
 }
 
 // 从某个位置开始播放 ms
 - (void)seekToTimePlay:(NSInteger)time {
-    if (_isPlaying) {
-        [_player pause];
-    }
     _isPlaying = YES;
-    MJWeakSelf;
     CMTime offsetCTTime = CMTimeMake(labs(time), 1000);
+    @weakObj(self);
     CMTime toleranceTime = CMTimeMake(1, 1000);
-    [_player seekToTime:offsetCTTime toleranceBefore:toleranceTime toleranceAfter:toleranceTime completionHandler:^(BOOL finished) {
-        [weakSelf.player play];
+    [self.player ss_seekToTime:offsetCTTime toleranceBefore:toleranceTime toleranceAfter:toleranceTime completionHandler:^(BOOL finished) {
+        @strongObj(self);
+        [self.player play];
+    }];
+}
 
+// 调整进度到某个位置
+- (void)seekToTime:(NSInteger)time callback:(void(^)(void))callback {
+    [self.player pause];
+    CMTime offsetCTTime = CMTimeMake(labs(time), 1000);
+    CMTime toleranceTime = CMTimeMake(1, 1000);
+    [self.player ss_seekToTime:offsetCTTime toleranceBefore:toleranceTime toleranceAfter:toleranceTime completionHandler:^(BOOL finished) {
+        callback();
     }];
 }
 
 - (void)seekOffsetTime:(NSInteger)offsetTime {
     CMTime newTime = CMTimeMake(offsetTime, 1000);
     CMTime toleranceTime = CMTimeMake(1, 1000);
-    [_player seekToTime:newTime toleranceBefore:toleranceTime toleranceAfter:toleranceTime completionHandler:^(BOOL finished) {
+    
+    [self.player pause];
+    @weakObj(self);
+    [self.player ss_seekToTime:newTime toleranceBefore:toleranceTime toleranceAfter:toleranceTime completionHandler:^(BOOL finished) {
+        @strongObj(self);
+        if (self.isPlaying) {
+            [self.player play];
+        }
+    }];
+}
+
+// 调整进度
+- (void)seekOffsetTimeNoPuase:(NSInteger)offsetTime {
+    CMTime newTime = CMTimeMake(offsetTime, 1000);
+    CMTime toleranceTime = CMTimeMake(1, 1000);
+
+    [self.player ss_seekToTime:newTime toleranceBefore:toleranceTime toleranceAfter:toleranceTime completionHandler:^(BOOL finished) {
 
     }];
 }
@@ -197,7 +252,7 @@
     __block typeof(self) bself = self;
     _timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 100) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
         //设置player的声音
-//        [bself setPlayerVolume];
+        //        [bself setPlayerVolume];
         //当前时间
         float current = CMTimeGetSeconds(time);
         //总共时间
@@ -205,11 +260,11 @@
         //进度
         float progress = current/total;
         //将值传入知道delegate方法中
-//        NSLog(@"--current %f -- total %f -----%f", current, total, progress);
+        //        NSLog(@"--current %f -- total %f -----%f", current, total, progress);
         if (bself.delegate && [bself.delegate respondsToSelector:@selector(getSongCurrentTime:andTotalTime:andProgress:currentInterval:playTime:inPlayer:)]) {
             NSDate *date = [NSDate date];
             NSTimeInterval inteveral = [date timeIntervalSince1970];
-//            NSLog(@"----- start play %f", inteveral * 1000);
+            //            NSLog(@"----- start play %f", inteveral * 1000);
             [bself.delegate getSongCurrentTime:current*1000  andTotalTime:total*1000 andProgress:progress currentInterval:inteveral playTime:current*1000 inPlayer:bself];
         }
     }];
@@ -256,8 +311,14 @@
                 NSLog(@"未知状态,此时不能播放");
             }
                 break;
-            case AVPlayerStatusReadyToPlay:{
+            case AVPlayerStatusReadyToPlay: {
+                _isReady = YES;
                 NSLog(@"准备完毕,可以播放");
+                dispatch_main_sync_safe(^{
+                    if (self.delegate && [self.delegate respondsToSelector:@selector(playerIsReadyPlay:)]) {
+                        [self.delegate playerIsReadyPlay:self];
+                    }
+                });
             }
                 break;
             case AVPlayerStatusFailed:{
@@ -321,9 +382,11 @@
 {
     //移除所有监听
     _isPlaying = NO;
-    if ([self.delegate respondsToSelector:@selector(playFinished:)]) {
-        [self.delegate playFinished:self];
-    }
+    dispatch_main_sync_safe(^{
+        if ([self.delegate respondsToSelector:@selector(playFinished:)]) {
+            [self.delegate playFinished:self];
+        }
+    });
 }
 
 #pragma mark----移除通知
@@ -351,12 +414,13 @@
 - (void)dealloc {
     NSLog(@" -------- player dealloc ");
 }
+
 /*
-// Only override drawRect: if you perform custom drawing.
-// An empty implementation adversely affects performance during animation.
-- (void)drawRect:(CGRect)rect {
-    // Drawing code
-}
-*/
+ // Only override drawRect: if you perform custom drawing.
+ // An empty implementation adversely affects performance during animation.
+ - (void)drawRect:(CGRect)rect {
+ // Drawing code
+ }
+ */
 
 @end

+ 27 - 26
KulexiuForTeacher/KulexiuForTeacher/Common/Tools/VideoEditor/KSVideoEditor.m

@@ -53,7 +53,7 @@
     // 视频需要专设置transform
     CGAffineTransform videoTransform = [[videoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0].preferredTransform;
     [a_compositionVideoTrack setPreferredTransform:videoTransform];
-
+    
     CMTime start = CMTimeMakeWithSeconds(startTime, videoAsset.duration.timescale);
     CMTime duration = CMTimeMakeWithSeconds(endTime - startTime,videoAsset.duration.timescale);
     CMTimeRange audio_timeRange = CMTimeRangeMake(start, duration);
@@ -69,9 +69,9 @@
                            ofTrack:[[audioAsset tracksWithMediaType:AVMediaTypeAudio]objectAtIndex:0]
                             atTime:start
                              error:nil];
-
+    
     AVMutableAudioMixInputParameters *newAudioInputParams = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:newAudioTrack] ;
-//    [newAudioInputParams setVolumeRampFromStartVolume:newVolume toEndVolume:.0f timeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.duration)];
+    //    [newAudioInputParams setVolumeRampFromStartVolume:newVolume toEndVolume:.0f timeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.duration)];
     [newAudioInputParams setTrackID:newAudioTrack.trackID];
     
     // 添加背景音URL
@@ -80,11 +80,11 @@
         
         AVURLAsset *bgAudioAsset = [[AVURLAsset alloc]initWithURL:bgAudio_url options:nil];
         AVMutableCompositionTrack *newBgAudioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio
-                                                                               preferredTrackID:kCMPersistentTrackID_Invalid];
+                                                                                 preferredTrackID:kCMPersistentTrackID_Invalid];
         [newBgAudioTrack insertTimeRange:audio_timeRange
-                               ofTrack:[[bgAudioAsset tracksWithMediaType:AVMediaTypeAudio]objectAtIndex:0]
-                                atTime:start
-                                 error:nil];
+                                 ofTrack:[[bgAudioAsset tracksWithMediaType:AVMediaTypeAudio]objectAtIndex:0]
+                                  atTime:start
+                                   error:nil];
         
         AVMutableAudioMixInputParameters *bgAudioInputParams = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:newBgAudioTrack];
         [bgAudioInputParams setTrackID:newBgAudioTrack.trackID];
@@ -103,11 +103,11 @@
         [originAudioInputParams setTrackID:originVoiceTrack.trackID];
         if (bgAudioInputParams) {
             audioMix.inputParameters = [NSArray arrayWithObjects:newAudioInputParams,bgAudioInputParams,originAudioInputParams,nil];
-
+            
         }
         else {
             audioMix.inputParameters = [NSArray arrayWithObjects:newAudioInputParams,originAudioInputParams,nil];
-
+            
         }
     }else{
         audioMix.inputParameters = [NSArray arrayWithObjects:newAudioInputParams,bgAudioInputParams,nil];
@@ -122,23 +122,24 @@
     
     [_assetExport exportAsynchronouslyWithCompletionHandler:
      ^(void ) {
-        switch ([_assetExport status]) {
-            case AVAssetExportSessionStatusFailed: {
-                NSLog(@"合成失败:%@",[[_assetExport error] description]);
-                completionHandle(outputFilePath,NO);
-            } break;
-            case AVAssetExportSessionStatusCancelled: {
-                completionHandle(outputFilePath,NO);
-            } break;
-            case AVAssetExportSessionStatusCompleted: {
-                completionHandle(outputFilePath,YES);
-            } break;
-            default: {
-                completionHandle(outputFilePath,NO);
-            } break;
-        }
-    }
-     ];
+        dispatch_main_async_safe(^{
+            switch ([_assetExport status]) {
+                case AVAssetExportSessionStatusFailed: {
+                    NSLog(@"合成失败:%@",[[_assetExport error] description]);
+                    completionHandle(outputFilePath,NO);
+                } break;
+                case AVAssetExportSessionStatusCancelled: {
+                    completionHandle(outputFilePath,NO);
+                } break;
+                case AVAssetExportSessionStatusCompleted: {
+                    completionHandle(outputFilePath,YES);
+                } break;
+                default: {
+                    completionHandle(outputFilePath,NO);
+                } break;
+            }
+        });
+    }];
 }
 
 + (NSString *)fileSavePath

+ 1 - 0
KulexiuForTeacher/KulexiuForTeacher/Module/Mine/Works/Controller/MineWorksViewController.h

@@ -17,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
 @property (nonatomic, strong, readonly) JXCategoryTitleView *categoryView;
 @property (nonatomic, strong) NSArray <NSString *> *titles;
 
+- (void)displayDraft:(BOOL)displayDraft;
 
 @end
 

+ 11 - 1
KulexiuForTeacher/KulexiuForTeacher/Module/Mine/Works/Controller/MineWorksViewController.m

@@ -20,10 +20,15 @@
 @property (nonatomic, assign) BOOL isEdit;
 
 @property (nonatomic, assign) NSInteger selectedIndex;
+
+@property (nonatomic, assign) BOOL displayDraftPage;
+
 @end
 
 @implementation MineWorksViewController
-
+- (void)displayDraft:(BOOL)displayDraft {
+    self.displayDraftPage = displayDraft;
+}
 - (void)viewDidLoad {
     [super viewDidLoad];
     // Do any additional setup after loading the view.
@@ -83,6 +88,11 @@
     self.pagerView.listContainerView.listCellBackgroundColor = [UIColor clearColor];
 
     self.categoryView.listContainer = (id<JXCategoryViewListContainer>)self.pagerView.listContainerView;
+    if (self.displayDraftPage) {
+        self.selectedIndex = 1;
+        [self.categoryView setDefaultSelectedIndex:self.selectedIndex];
+        self.navView.showEditButton = YES;
+    }
 }
 
 - (void)viewDidAppear:(BOOL)animated {