Files
fmviewer3/project/fm_viewer/data/fm_video_edit.cpp
2026-02-21 17:11:31 +09:00

462 lines
18 KiB
C++

#include "fm_video_edit.h"
#if (FM_VIDEO_EDIT)
#if (USE_LIB_MP4V2)
#include "mp4v2.h"
#else
#ifdef __cplusplus
extern "C"
{
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
}
#endif // extern C
#endif //USE_LIB_MP4V2
#define MAX_VIDEO_STREAM_COUNT 10
FMVideoEdit::FMVideoEdit(QStringList files, QStringList dests, EditMode mode, bool deleteSrcDone)
{
_files = files;
_dests = dests;
_mode = mode;
_deleteSrcAfter = deleteSrcDone;
}
void FMVideoEdit::run()
{
_splitMP4VideoTracks();
}
int _find_video_stream_index(int stream_id, int *streams) {
for(int i=0;i<MAX_VIDEO_STREAM_COUNT;i++) {
if(stream_id == streams[i]) {
return i;
}
}
return -1;
}
void _clean_output_contexts(AVFormatContext** ctxs) {
for(int i=0;i<MAX_VIDEO_STREAM_COUNT;i++) {
if (ctxs[i] != NULL) {
avformat_free_context(ctxs[i]);
}
}
}
void FMVideoEdit::_splitMP4VideoTracks()
{
QString src = _files.first();
AVFormatContext *input_ctx = NULL;
int video_stream_index[MAX_VIDEO_STREAM_COUNT] = {-1,};
bool video_stream_validation[MAX_VIDEO_STREAM_COUNT] = {true,}; // 정상 Video Stream 확인
int video_stream_count = 0;
int real_video_stream_count = 0; // 실제 비디오 스트림 개수
int audio_stream_index = -1; // Audio 가 존재하지 않을 경우 stream index 를 2가 아닌 1로 작성하기 위해 사용
int video_frame_count = -1; // Progress 처리하기 위해 사용
// 입력 파일 열기
if (avformat_open_input(&input_ctx, src.toUtf8().constData(), NULL, NULL) < 0) {
emit done(ErrorFileOpen);
return;
}
// 메타 정보 확인
// major_brand : isom
// minor_version : 512
// compatible_brands : isomiso2avc1mp41
/*
AVDictionaryEntry *tag = NULL;
while ((tag = av_dict_get(input_ctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) {
qInfo() << tag->key << ":" << tag->value << __FUNCTION__;
}
*/
// 스트림 정보 읽기
if (avformat_find_stream_info(input_ctx, NULL) < 0) {
emit done(ErrorOpenStream);
avformat_close_input(&input_ctx);
return;
}
// 각 비디오 스트림 + 오디오/자막 스트림 인덱스 확인
for (unsigned int i = 0; i < input_ctx->nb_streams; i++) {
AVMediaType type = input_ctx->streams[i]->codecpar->codec_type;
if (type == AVMEDIA_TYPE_VIDEO) {
// 프레임이 없는 N 번째 영상 스트림 제거
video_stream_validation[i] = (input_ctx->streams[i]->nb_frames > 0);
// if(input_ctx->streams[i]->nb_frames == 0) {
// video_stream_validation[i] = false;
// }
if(video_stream_validation[i])
{
real_video_stream_count += 1;
}
// Progress 처리하기 위해
if(video_frame_count < 0) {
video_frame_count = input_ctx->streams[i]->nb_frames;
}
// AVStream* ps = input_ctx->streams[i];
// qInfo() << "VSTREAM:" << i << ps->discard << __FUNCTION__;
video_stream_index[video_stream_count] = i;
video_stream_count++;
} else if (type == AVMEDIA_TYPE_AUDIO) {
audio_stream_index = i;
} //else if (type == AVMEDIA_TYPE_SUBTITLE) {
// subtitle_stream_index = i;
//}
}
// 실제 영상 스트림이 출력 개수보다 많을 경우에만 처리
//video_stream_count = 2;
if(real_video_stream_count > _dests.size()) {
emit done(ErrorWrongTrackCount);
avformat_close_input(&input_ctx);
return;
}
// 한번에 모든 패킷을 여러 파일에 저장하기 위해 동시 생성
AVFormatContext *output_ctx[MAX_VIDEO_STREAM_COUNT] = {NULL,};
AVStream *out_video_stream[MAX_VIDEO_STREAM_COUNT] = {NULL,};
AVStream *out_audio_stream[MAX_VIDEO_STREAM_COUNT] = {NULL,};
AVStream *out_subtitle_stream[MAX_VIDEO_STREAM_COUNT] = {NULL,};
for(int s=0;s<video_stream_count;s++) {
// 프레임 없는 영상 스트림 SKIP
if(!video_stream_validation[s]) {
continue;
}
// 복사할 비디오 스트림 인덱스
// int current_video_stream = video_stream_index[s];
// 출력 파일 생성
//AVFormatContext *output_ctx = NULL;
QString destString = _dests.at(s);
char dest[4096] = {0,};
memcpy(dest,destString.toUtf8().constData(),destString.toUtf8().length());
// MP4는 PCM 데이터를 저장하지 못하므로 확장자는 MP4 이나 강제로 MOV 포멧으로 지정
avformat_alloc_output_context2(&output_ctx[s], NULL, "mov", dest);
if (!output_ctx[s]) {
emit done(ErrorFileCreate);
avformat_close_input(&input_ctx);
_clean_output_contexts(output_ctx);
return;
}
// 비디오 스트림 생성
out_video_stream[s] = avformat_new_stream(output_ctx[s], NULL);
if (!out_video_stream[s]) {
emit done(ErrorCreateStream);
_clean_output_contexts(output_ctx);
avformat_close_input(&input_ctx);
return;
}
// 스트림 파라미터 복사
int ret = avcodec_parameters_copy(out_video_stream[s]->codecpar, input_ctx->streams[video_stream_index[s]]->codecpar);
if (ret < 0) {
emit done(ErrorCopyStreamParameters);
_clean_output_contexts(output_ctx);
avformat_close_input(&input_ctx);
return;
}
out_video_stream[s]->codecpar->codec_tag = 0;
// 나머지 스트림들 복사
for (unsigned int i = 0; i < input_ctx->nb_streams; i++) {
AVMediaType type = input_ctx->streams[i]->codecpar->codec_type;
if (type == AVMEDIA_TYPE_AUDIO) {
out_audio_stream[s] = avformat_new_stream(output_ctx[s], NULL);
if (!out_audio_stream[s]) {
emit done(ErrorCreateStream);
_clean_output_contexts(output_ctx);
avformat_close_input(&input_ctx);
return;
}
ret = avcodec_parameters_copy(out_audio_stream[s]->codecpar, input_ctx->streams[i]->codecpar);
if (ret < 0) {
emit done(ErrorCopyStreamParameters);
_clean_output_contexts(output_ctx);
avformat_close_input(&input_ctx);
return;
}
out_audio_stream[s]->codecpar->codec_tag = 0;
}
// 자막 스트림 복사
if (type == AVMEDIA_TYPE_SUBTITLE) {
out_subtitle_stream[s] = avformat_new_stream(output_ctx[s], NULL);
if (!out_subtitle_stream[s]) {
emit done(ErrorCreateStream);
_clean_output_contexts(output_ctx);
avformat_close_input(&input_ctx);
return;
}
ret = avcodec_parameters_copy(out_subtitle_stream[s]->codecpar, input_ctx->streams[i]->codecpar);
if (ret < 0) {
emit done(ErrorCopyStreamParameters);
_clean_output_contexts(output_ctx);
avformat_close_input(&input_ctx);
return;
}
out_subtitle_stream[s]->codecpar->codec_tag = 0;
}
} // 나머지 스트림 복사
// 출력 파일 헤더 쓰기
av_dump_format(output_ctx[s], 0, dest, 1);
if (!(output_ctx[s]->oformat->flags & AVFMT_NOFILE)) {
if (avio_open(&output_ctx[s]->pb, dest, AVIO_FLAG_WRITE) < 0) {
emit done(ErrorFileWrite);
_clean_output_contexts(output_ctx);
avformat_close_input(&input_ctx);
return;
}
}
// major_brand를 'isom'으로 설정
// AVDictionary *options = NULL;
// av_dict_set(&options, "major_brand", "isom", 0);
// av_dict_set(&options, "compatible_brands", "isomiso2avc1mp41", 0);
//&options
int res = avformat_write_header(output_ctx[s], NULL); // &options
if (res < 0)
{
//char ebuffer[1024] = {0,};
//av_strerror(res,ebuffer,1024);
qInfo() << "VIDEO STREAM:" << s << " HEADER WRITE ERROR" << __FUNCTION__;
emit done(ErrorFileWrite);
_clean_output_contexts(output_ctx);
avformat_close_input(&input_ctx);
// av_dict_free(&options);
return;
}
} // 파일 생성 및 스트림 정보 복사
// 패킷 복사
AVPacket packet;
// for 문으로 처리시 처음으로 다시 돌아가는 기능 필요...
// av_seek_frame(input_ctx, -1, 0, AVSEEK_FLAG_BACKWARD);
int frame_processed = 0; // Progress 처리
while (av_read_frame(input_ctx, &packet) >= 0) {
AVMediaType type = input_ctx->streams[packet.stream_index]->codecpar->codec_type;
// 입력 스트림과 출력 스트림의 타임베이스 정보를 가져옵니다.
const AVRational& in_time_base = input_ctx->streams[packet.stream_index]->time_base;
if(type == AVMEDIA_TYPE_VIDEO) {
// video 순서 index 확인
int vindex = _find_video_stream_index(packet.stream_index,video_stream_index);
if(!video_stream_validation[vindex]) {
continue;
}
// 시간계산 비디오 스트림 인덱스는 무조건 0
const AVRational& out_time_base = output_ctx[vindex]->streams[0]->time_base;
// 모든 패킷 RESCALE 하지 않으면 재생시간 짧아짐
packet.pts = av_rescale_q(packet.pts, in_time_base, out_time_base);
packet.dts = av_rescale_q(packet.dts, in_time_base, out_time_base);
packet.duration = av_rescale_q(packet.duration, in_time_base, out_time_base);
packet.stream_index = 0; // 출력 비디오 스트림 인덱스
av_interleaved_write_frame(output_ctx[vindex], &packet);
if(vindex == 0 && video_frame_count > 0) {
frame_processed++;
int percent = int (((double)frame_processed) / ((double)video_frame_count) * 100.0);
emit progress(percent);
//qInfo() << progress << __FUNCTION__;
}
} else if (type == AVMEDIA_TYPE_AUDIO) {
// 패킷을 여러번 쓰기 위해서는 av_packet_clone 해서 처리해야함..
for(int s=0;s<video_stream_count;s++) { // 모든 파일에 오디오 패킷 쓰기
if(!video_stream_validation[s]) { // invalid video 스트림 SKIP
continue;
}
// 패킷 시간 RESCALE 처리해아함 // AUDIO STREAM INDEX 는 무조건 1
const AVRational& out_time_base = output_ctx[s]->streams[1]->time_base;
AVPacket* cp = av_packet_clone(&packet);
// 모든 패킷 RESCALE 하지 않으면 재생시간 짧아짐
cp->pts = av_rescale_q(cp->pts, in_time_base, out_time_base);
cp->dts = av_rescale_q(cp->dts, in_time_base, out_time_base);
cp->duration = av_rescale_q(cp->duration, in_time_base, out_time_base);
cp->stream_index = 1; //출력 오디오 스트림 인덱스
av_interleaved_write_frame(output_ctx[s], cp);
av_packet_free(&cp);
}
} else if (type == AVMEDIA_TYPE_SUBTITLE) {
// AUDIO 가 존재하지 않을경우 1 존재할 경우 2
const int subtitle_stream_index = audio_stream_index >= 0 ? 2 : 1;
for(int s=0;s<video_stream_count;s++) { // 모든 파일에 자막 패킷 쓰기
if(!video_stream_validation[s]) { // invalid video 스트림 SKIP
continue;
}
// 패킷 시간 RESCALE 처리해아함
const AVRational& out_time_base = output_ctx[s]->streams[subtitle_stream_index]->time_base;
AVPacket* cp = av_packet_clone(&packet);
// 모든 패킷 RESCALE 하지 않으면 재생시간 짧아짐 for 에서 누적되니 신규 packet 에만 처리
cp->pts = av_rescale_q(cp->pts, in_time_base, out_time_base);
cp->dts = av_rescale_q(cp->dts, in_time_base, out_time_base);
cp->duration = av_rescale_q(cp->duration, in_time_base, out_time_base);
cp->stream_index = subtitle_stream_index;
av_interleaved_write_frame(output_ctx[s], cp);
av_packet_free(&cp);
}
}
av_packet_unref(&packet);
}
for(int s=0;s<video_stream_count;s++) {
if(!video_stream_validation[s]) { // invalid video 스트림 파일 SKIP
continue;
}
// 트레일러 쓰기 및 파일 닫기
av_write_trailer(output_ctx[s]);
if (!(output_ctx[s]->oformat->flags & AVFMT_NOFILE)) {
avio_closep(&output_ctx[s]->pb);
}
//av_dict_free(&options);
avformat_free_context(output_ctx[s]);
}
avformat_close_input(&input_ctx);
_afterProcess();
emit done(ErrorNone);
}
void FMVideoEdit::_afterProcess()
{
if(_deleteSrcAfter) {
for(int i=0;i<_files.size();i++) {
QFile(_files.at(i)).remove();
qInfo() << "DELETE:" << _files.at(i) << __FUNCTION__;
}
}
}
#if (USE_LIB_MP4V2)
void FMVideoEdit::_splitVideoTracks()
{
QString src = _files.first();
/* 최적화 하여 마지막 이상한 atom 제거해도 수정이 안됨
QFileInfo fi = QFileInfo(src2);
QString src = QDir::cleanPath(fi.dir().path() + QDir::separator() + fi.baseName() + "_2.mp4");
if(!MP4Optimize(src2.toLocal8Bit().data(),src.toLocal8Bit().data())) {
//qInfo() << "OPTIMIZE FAILED.." << __FUNCTION__;
return;
} */
MP4FileHandle inputFile = MP4Read(src.toLocal8Bit().data());
if(MP4_INVALID_FILE_HANDLE == inputFile) {
emit done(ErrorFileOpen);
return;
}
uint32_t numTrack = MP4GetNumberOfTracks(inputFile,NULL,0);
//qInfo() << QString(MP4Info(mp4File)) << __FUNCTION__;
qInfo() << "numTrack" << numTrack << __FUNCTION__;
QList<MP4TrackId> videoTracks = QList<MP4TrackId>();
QList<MP4TrackId> otherTracks = QList<MP4TrackId>(); // 영상을 제외한 트랙들
for(uint32_t i=0;i<numTrack;i++) {
MP4TrackId tid = MP4FindTrackId (inputFile, i, NULL,0);
const char* type = MP4GetTrackType(inputFile,tid);
if(strcmp(type,MP4_VIDEO_TRACK_TYPE) == 0) {
videoTracks.append(tid);
qInfo() << "TRACK:" << i << " ID:" << tid << " TYPE:" << type << __FUNCTION__;
} else if (strcmp(type,MP4_AUDIO_TRACK_TYPE) != 0) { // else { //
// 오디오 트랙만 처리하면 atom size 에러가 발생함...
otherTracks.append(tid);
qInfo() << "OTHER TRACK:" << i << " ID:" << tid << " TYPE:" << type << __FUNCTION__;
}
}
if(videoTracks.size() != _dests.size()) {
emit done(ErrorWrongTrackCount);
MP4Close(inputFile);
}
for(int i=0;i<_dests.size();i++) {
QString dest = _dests.at(i);
MP4FileHandle outputFile = MP4Create(dest.toLocal8Bit().data());
if (outputFile == MP4_INVALID_FILE_HANDLE) {
emit done(ErrorFileCreate);
MP4Close(inputFile);
return;
}
// 입력 파일의 일반적인 정보를 출력 파일에 복사
MP4SetTimeScale(outputFile, MP4GetTimeScale(inputFile));
QList<MP4TrackId> cloneTracks = QList<MP4TrackId>();
cloneTracks.append(videoTracks.at(i)); // 영상 트랙
cloneTracks.append(otherTracks); // 나머지 트랙(자막,메타,음성)
qInfo() << "cloneTracks:" << cloneTracks << __FUNCTION__;
// 트랙 복제
for (int j=0;j<cloneTracks.size();j++) {
MP4TrackId srcID = cloneTracks.at(j);
MP4TrackId outputVideoTrackId = MP4CloneTrack(inputFile, srcID, outputFile);
qInfo() << "CLONE TRACK SRC:" << srcID << " OUT:" << outputVideoTrackId << __FUNCTION__;
if (outputVideoTrackId == MP4_INVALID_TRACK_ID) {
MP4Close(inputFile);
MP4Close(outputFile);
emit done(ErrorTrackClone);
return;
}
// 샘플 복사
uint32_t sampleId = 1;
MP4SampleId sampleCount = MP4GetTrackNumberOfSamples(inputFile, srcID);
while (sampleId <= sampleCount) {
uint8_t* sampleData = nullptr;
uint32_t sampleSize = 0;
MP4Timestamp startTime = 0;
MP4Duration sampleDuration = 0;
if (MP4ReadSample(inputFile, srcID, sampleId, &sampleData, &sampleSize,&startTime, &sampleDuration)) {
if (!MP4WriteSample(outputFile, outputVideoTrackId, sampleData, sampleSize, sampleDuration)) {
emit done(ErrorWriteSample);
MP4Free(sampleData);
break;
}
MP4Free(sampleData);
} else {
emit done(ErrorReadSample);
break;
}
sampleId++;
}
} // 각 트랙 복사
MP4Close(outputFile);
}
MP4Close(inputFile);
}
#endif // #if (USE_LIB_MP4V2)
#endif // #if (FM_VIDEO_EDIT)