#include "MKVPlayer.h" #include "api__in_mkv.h" #include "main.h" #include "../Winamp/wa_ipc.h" #include "../nsmkv/nsmkv.h" #include "../nu/AutoLock.h" #include "../nu/ns_wc.h" #include "../nu/AutoWide.h" #include "../nu/AudioOutput.h" #include "ifc_mkvaudiodecoder.h" #include "svc_mkvdecoder.h" #include "ifc_mkvvideodecoder.h" #include "../nsmkv/file_mkv_reader.h" #include #include #include #include // {B6CB4A7C-A8D0-4c55-8E60-9F7A7A23DA0F} static const GUID playbackConfigGroupGUID = { 0xb6cb4a7c, 0xa8d0, 0x4c55, { 0x8e, 0x60, 0x9f, 0x7a, 0x7a, 0x23, 0xda, 0xf } }; IVideoOutput *videoOutput = 0; /* benski> TODO: keep track of "fully parsed position" we don't have to always start over at segment_position TODO: if we have multiple audio or video tracks, do that weird winamp interface for it */ void MKVPlayer::MKVWait::Wait_SetEvents(HANDLE killswitch, HANDLE seek_event) { handles[0]=killswitch; handles[1]=seek_event; } int MKVPlayer::MKVWait::WaitOrAbort(int time_in_ms) { switch(WaitForMultipleObjects(2, handles, FALSE, 55)) { case WAIT_TIMEOUT: // all good, wait successful return 0; case WAIT_OBJECT_0: // killswitch return MKVPlayer::MKV_STOP; case WAIT_OBJECT_0+1: // seek event return MKVPlayer::MKV_ABORT; default: // some OS error? return MKVPlayer::MKV_ERROR; } } MKVPlayer::MKVPlayer(const wchar_t *_filename) : audio_output(&plugin) { first_cluster_found = false; cues_searched = false; segment_position=0; segment_size = 0; filename = _wcsdup(_filename); m_needseek = -1; audio_decoder=0; audio_track_num=0; audio_output_len = 65536; audio_opened=false; audio_flushing=FLUSH_START; audio_first_timestamp=0; audio_buffered=0; audio_bitrate=0; video_decoder=0; video_track_num=0; video_opened = false; video_stream = 0; video_thread = 0; video_timecode_scale = 0; video_cluster_position = 0; video_track_entry = 0; video_bitrate = 0; consecutive_early_frames = 0; if (!videoOutput) videoOutput = (IVideoOutput *)SendMessage(plugin.hMainWindow, WM_WA_IPC, 0, IPC_GET_IVIDEOOUTPUT); killswitch = CreateEvent(NULL, TRUE, FALSE, NULL); seek_event = CreateEvent(NULL, TRUE, FALSE, NULL); /* video events */ video_break = CreateEvent(NULL, TRUE, FALSE, NULL); video_flush_done = CreateEvent(NULL, FALSE, FALSE, NULL); video_flush = CreateEvent(NULL, TRUE, FALSE, NULL); video_resume = CreateEvent(NULL, TRUE, FALSE, NULL); video_ready = CreateEvent(NULL, TRUE, FALSE, NULL); audio_output.Wait_SetEvents(killswitch, seek_event); } MKVPlayer::~MKVPlayer() { free(filename); CloseHandle(killswitch); CloseHandle(seek_event); CloseHandle(video_break); CloseHandle(video_flush_done); CloseHandle(video_flush); CloseHandle(video_resume); CloseHandle(video_ready); if (audio_decoder) { audio_decoder->Close(); } delete main_reader; } void MKVPlayer::Kill() { SetEvent(killswitch); if (video_thread) WaitForSingleObject(video_thread, INFINITE); video_thread = 0; if (video_decoder) video_decoder->Close(); video_decoder=0; } void MKVPlayer::Seek(int seek_pos) { m_needseek = seek_pos; SetEvent(seek_event); } int MKVPlayer::GetOutputTime() const { if (m_needseek != -1) return m_needseek; else return plugin.outMod->GetOutputTime() + audio_first_timestamp; } DWORD CALLBACK MKVThread(LPVOID param) { MKVPlayer *player = (MKVPlayer *)param; DWORD ret = player->ThreadFunction(); return ret; } bool MKVPlayer::FindCues() { if (cues_searched) return true; uint64_t original_position = main_reader->Tell(); // first, let's try the seek table uint64_t cues_position = 0; if (seek_table.GetEntry(mkv_segment_cues, &cues_position)) { main_reader->Seek(cues_position+segment_position); ebml_node node; if (read_ebml_node(main_reader, &node) == 0) return false; if (node.id == mkv_segment_cues) // great success! { if (nsmkv::ReadCues(main_reader, node.size, cues) == 0) return false; cues_searched=true; main_reader->Seek(original_position); return true; } } main_reader->Seek(segment_position); // TODO: keep track of how far Step() has gotten so we don't have to start from scratch /* --- TODO: make this block into a function in nsmkv --- */ while (1) // TODO: key off segment size to make sure we don't overread { uint64_t this_position = main_reader->Tell(); ebml_node node; if (read_ebml_node(main_reader, &node) == 0) return false; if (node.id != mkv_void) { nsmkv::SeekEntry seek_entry(node.id, this_position-segment_position); seek_table.AddEntry(seek_entry, nsmkv::SeekTable::ADDENTRY_FOUND); } if (node.id == mkv_segment_cues) { if (nsmkv::ReadCues(main_reader, node.size, cues) == 0) return false; break; } else { main_reader->Skip(node.size); } } /* ------ */ cues_searched=true; main_reader->Seek(original_position); return true; } bool MKVPlayer::ParseHeader() { ebml_node node; if (read_ebml_node(main_reader, &node) == 0) return false; if (node.id != mkv_header) return false; if (nsmkv::ReadHeader(main_reader, node.size, header) == 0) return false; if (OnHeader(header) != MKV_CONTINUE) return false; return true; } bool MKVPlayer::FindSegment() { ebml_node node; while (segment_position == 0) { if (read_ebml_node(main_reader, &node) == 0) return false; if (node.id == mkv_segment) { segment_position = main_reader->Tell(); segment_size = node.size; } else { if (nsmkv::SkipNode(main_reader, node.id, node.size) == 0) return false; } } return true; } static ifc_mkvvideodecoder *FindVideoDecoder(const nsmkv::TrackEntry *track_entry) { size_t n = 0; waServiceFactory *sf = 0; while (sf = plugin.service->service_enumService(WaSvc::MKVDECODER, n++)) { svc_mkvdecoder *dec = static_cast(sf->getInterface()); if (dec) { ifc_mkvvideodecoder *decoder=0; if (dec->CreateVideoDecoder(track_entry->codec_id, track_entry, &track_entry->video, &decoder) == svc_mkvdecoder::CREATEDECODER_SUCCESS) { sf->releaseInterface(dec); return decoder; } sf->releaseInterface(dec); } } return 0; } static ifc_mkvaudiodecoder *FindAudioDecoder(const nsmkv::TrackEntry *track_entry) { unsigned int bits_per_sample = (unsigned int)AGAVE_API_CONFIG->GetUnsigned(playbackConfigGroupGUID, L"bits", 16); if (bits_per_sample >= 24) bits_per_sample = 24; else bits_per_sample = 16; unsigned int max_channels; // get max channels if (AGAVE_API_CONFIG->GetBool(playbackConfigGroupGUID, L"surround", true)) max_channels = 6; else if (AGAVE_API_CONFIG->GetBool(playbackConfigGroupGUID, L"mono", false)) max_channels = 1; else max_channels = 2; size_t n=0; waServiceFactory *sf = 0; while (sf = plugin.service->service_enumService(WaSvc::MKVDECODER, n++)) { svc_mkvdecoder *dec = static_cast(sf->getInterface()); if (dec) { ifc_mkvaudiodecoder *decoder=0; // TODO: read from api_config!! if (dec->CreateAudioDecoder(track_entry->codec_id, track_entry, &track_entry->audio, bits_per_sample, max_channels, false, &decoder) == svc_mkvdecoder::CREATEDECODER_SUCCESS) { sf->releaseInterface(dec); return decoder; } sf->releaseInterface(dec); } } return 0; } int MKVPlayer::OnAudio(const nsmkv::Cluster &cluster, const nsmkv::BlockBinary &binary) { if (video_decoder) { HANDLE handles[3] = {killswitch, seek_event, video_ready}; if (WaitForMultipleObjects(3, handles, FALSE, INFINITE) != WAIT_OBJECT_0+2) return MKV_ABORT; } if (audio_flushing != FLUSH_NONE) { uint64_t timestamp=cluster.time_code + binary.time_code; uint64_t timestamp_ms = segment_info.time_code_scale * timestamp / 1000000ULL; if (audio_flushing == FLUSH_START || !audio_opened) { audio_first_timestamp = (int)timestamp_ms; } else { audio_first_timestamp=0; audio_output.Flush((int)timestamp_ms); m_needseek = -1; } audio_flushing=FLUSH_NONE; } nsmkv::LacingState lacing_state; uint32_t i = 0; if (nsmkv::Lacing::GetState(binary.flags, (const uint8_t *)binary.data, binary.data_size, &lacing_state)) { const uint8_t *frame = 0; size_t frame_len = 0; while (nsmkv::Lacing::GetFrame(i++, (const uint8_t *)binary.data, binary.data_size, &frame, &frame_len, &lacing_state)) { size_t decoded_size = audio_output_len-audio_buffered; if (audio_decoder->DecodeBlock((void *)frame, frame_len, audio_buffer+audio_buffered, &decoded_size) == ifc_mkvaudiodecoder::MKV_SUCCESS) { decoded_size+=audio_buffered; audio_buffered=0; if (!audio_opened) { unsigned int sample_rate, channels, bps; bool is_float; if (audio_decoder->GetOutputProperties(&sample_rate, &channels, &bps, &is_float) == ifc_mkvaudiodecoder::MKV_SUCCESS) { // TODO: pass -666 for 5th param if video if (!audio_output.Open(audio_first_timestamp, channels, sample_rate, bps, -1, -1)) { return MKV_STOP; } audio_opened=true; } } if (audio_opened && decoded_size) { int ret = audio_output.Write((char *)audio_buffer, decoded_size); if (ret != 0) return ret; } else { audio_buffered=decoded_size; } } } } return MKV_CONTINUE; } int MKVPlayer::OutputPictures(uint64_t default_timestamp) { void *data=0, *decoder_data=0; uint64_t timestamp=default_timestamp; while (video_decoder->GetPicture(&data, &decoder_data, ×tamp) == ifc_mkvvideodecoder::MKV_SUCCESS) { if (!video_opened) { int color_format = 0; int width = 0, height = 0; double aspect_ratio = 1.0; if (video_decoder->GetOutputProperties(&width, &height, &color_format, &aspect_ratio) == ifc_mkvvideodecoder::MKV_SUCCESS) { if (video_track_entry && video_track_entry->video.display_height && video_track_entry->video.display_width && video_track_entry->video.pixel_height && video_track_entry->video.pixel_width) { aspect_ratio = (double)video_track_entry->video.pixel_width / (double)video_track_entry->video.pixel_height / ((double)video_track_entry->video.display_width / (double)video_track_entry->video.display_height); } else { // winamp wants an "aspect correction value" not the true aspect ratio itself aspect_ratio = 1.0/aspect_ratio; } videoOutput->extended(VIDUSER_SET_THREAD_SAFE, 1, 0); videoOutput->open(width, height, 0, aspect_ratio, color_format); video_opened=true; SetEvent(video_ready); } } if (video_opened) { uint64_t timestamp_ms; if (video_timecode_scale == 0) timestamp_ms = segment_info.time_code_scale * timestamp / 1000000ULL; else timestamp_ms = (uint64_t) (video_timecode_scale * (double)segment_info.time_code_scale * (double)timestamp / 1000000.0); again: int realTime = plugin.outMod->GetOutputTime() + audio_first_timestamp; int time_diff = (int)timestamp_ms - realTime; if (time_diff > 12 && consecutive_early_frames) // plenty of time, go ahead and turn off frame dropping { if (--consecutive_early_frames == 0) video_decoder->HurryUp(0); } else if (time_diff < -50) // shit we're way late, start dropping frames { video_decoder->HurryUp(1); consecutive_early_frames += 3; } if (time_diff > 3) { HANDLE handles[] = {killswitch, video_break}; int ret= WaitForMultipleObjects(2, handles, FALSE, (DWORD)(timestamp_ms-realTime)); if (ret != WAIT_TIMEOUT) { video_decoder->FreePicture(data, decoder_data); if (ret == WAIT_OBJECT_0+1) /* second event doesn't stop stream*/ return MKV_ABORT; return MKV_STOP; } goto again; // TODO: handle paused state a little better than this } videoOutput->draw(data); } video_decoder->FreePicture(data, decoder_data); } return MKV_CONTINUE; } int MKVPlayer::OnVideo(const nsmkv::Cluster &cluster, const nsmkv::BlockBinary &binary) { if (video_decoder) { nsmkv::LacingState lacing_state; uint32_t i = 0; if (nsmkv::Lacing::GetState(binary.flags, (const uint8_t *)binary.data, binary.data_size, &lacing_state)) { const uint8_t *frame = 0; size_t frame_len = 0; while (nsmkv::Lacing::GetFrame(i++, (const uint8_t *)binary.data, binary.data_size, &frame, &frame_len, &lacing_state)) { // matroska epic fail: laced frames don't have separate timestamps! if (video_decoder) video_decoder->DecodeBlock(frame, frame_len, cluster.time_code+binary.time_code); uint64_t timestamp=cluster.time_code + binary.time_code; int ret = OutputPictures(timestamp); if (ret != MKV_CONTINUE) return ret; } } } return MKV_CONTINUE; } DWORD CALLBACK MKVPlayer::VideoThreadFunction() { //video_stream = _wfopen(filename, L"rb"); if (!video_stream) return 1; video_stream->Seek(video_cluster_position); HANDLE handles[] = { killswitch, video_break, video_flush, video_resume }; DWORD waitTime = 0; while (1) { int ret = WaitForMultipleObjects(4, handles, FALSE, waitTime); if (ret == WAIT_TIMEOUT) { int ret = Step(video_stream, &video_track_num, 1); if (ret == MKV_EOF) { video_decoder->EndOfStream(); OutputPictures(0); // TODO: tell decoder about end-of-stream to flush buffers waitTime = INFINITE; } else if (ret == MKV_ERROR || ret == MKV_STOP) { waitTime = INFINITE; // wait for killswitch } } else if (ret == WAIT_OBJECT_0) { break; } else if (ret == WAIT_OBJECT_0 + 1) // video break { waitTime = INFINITE; // this will stop us from decoding samples for a while ResetEvent(video_break); SetEvent(video_flush_done); } else if (ret == WAIT_OBJECT_0 + 2) // video flush { if (video_decoder) video_decoder->Flush(); ResetEvent(video_flush); waitTime = 0; SetEvent(video_flush_done); } else if (ret == WAIT_OBJECT_0 + 3) // resume video { ResetEvent(video_resume); waitTime = 0; SetEvent(video_flush_done); } } if (videoOutput) videoOutput->close(); delete video_stream; video_stream=0; return 0; } int MKVPlayer::OnHeader(const nsmkv::Header &header) { // TODO: figure out if the file is really matroska, and if we can support the ebml version return MKV_CONTINUE; } void MKVPlayer::OnSegmentInfo(const nsmkv::SegmentInfo &segment_info) { g_duration = segment_info.GetDurationMilliseconds(); uint64_t content_length = main_reader->GetContentLength(); if (content_length && g_duration) { int bitrate = (int)(8ULL * content_length / (uint64_t)g_duration); plugin.SetInfo(bitrate, -1, -1, -1); } } int MKVPlayer::OnTracks(const nsmkv::Tracks &tracks) { wchar_t audio_info[256] = {0}; wchar_t video_info[256] = {0}; // ===== enumerate tracks and find decoders ===== size_t i=0; const nsmkv::TrackEntry *track_entry; while (track_entry = tracks.EnumTrack(i++)) { if (track_entry->track_type == mkv_track_type_audio && !audio_decoder) { audio_decoder = FindAudioDecoder(track_entry); if (audio_decoder) { MultiByteToWideCharSZ(CP_UTF8, 0, track_entry->codec_id, -1, audio_info, 256); audio_track_num = track_entry->track_number; } } else if (track_entry->track_type == mkv_track_type_video && !video_decoder) { video_decoder = FindVideoDecoder(track_entry); if (video_decoder) { StringCbPrintfW(video_info, sizeof(video_info), L"%s %I64ux%I64u", AutoWide(track_entry->codec_id, CP_UTF8), track_entry->video.pixel_width, track_entry->video.pixel_height); video_track_num = track_entry->track_number; video_stream = new MKVReaderFILE(filename); video_timecode_scale = track_entry->track_timecode_scale; video_track_entry = track_entry; } } } // TODO this prevents trying to play video only files // which the plug-in is not at all happy playing /*if (!audio_decoder)// && !video_decoder) return MKV_STOP;*/ wchar_t video_status[512] = {0}; if (audio_decoder && video_decoder) { StringCbPrintf(video_status, sizeof(video_status), L"MKV: %s, %s", audio_info, video_info); videoOutput->extended(VIDUSER_SET_INFOSTRINGW,(INT_PTR)video_status,0); } else if (audio_decoder) { StringCbPrintf(video_status, sizeof(video_status), L"MKV: %s", audio_info); videoOutput->extended(VIDUSER_SET_INFOSTRINGW,(INT_PTR)video_status,0); } else if (video_decoder) { StringCbPrintf(video_status, sizeof(video_status), L"MKV: %s, %s", audio_info, video_info); videoOutput->extended(VIDUSER_SET_INFOSTRINGW,(INT_PTR)video_status,0); } return MKV_CONTINUE; } DWORD CALLBACK VideoThread(LPVOID param) { MKVPlayer *player = (MKVPlayer *)param; return player->VideoThreadFunction(); } int MKVPlayer::ParseCluster(nsmkv::MKVReader *stream, uint64_t size, uint64_t *track_numbers, size_t track_numbers_len) { nsmkv::Cluster cluster; uint64_t total_bytes_read=0; while (size) { ebml_node node; uint64_t bytes_read = read_ebml_node(stream, &node); if (bytes_read == 0) return MKV_ERROR; // benski> checking bytes_read and node.size separately prevents possible integer overflow attack if (bytes_read > size) return MKV_ERROR; total_bytes_read+=bytes_read; size-=bytes_read; if (node.size > size) return MKV_ERROR; total_bytes_read+=node.size; size-=node.size; switch(node.id) { case mkv_cluster_timecode: { uint64_t val; if (read_unsigned(stream, node.size, &val) == 0) return MKV_ERROR; printf("Time Code: %I64u\n", val); cluster.time_code = val; } break; case mkv_cluster_blockgroup: { printf("Block Group\n"); nsmkv::Block block; if (nsmkv::ReadBlockGroup(stream, node.size, block, track_numbers, track_numbers_len) == 0) return MKV_ERROR; if (block.binary.data) { int ret = OnBlock(cluster, block); if (ret != MKV_CONTINUE) return ret; } } break; case mkv_cluster_simpleblock: { printf("simple block, size: %I64u\n", node.size); nsmkv::Block block; if (ReadBlockBinary(stream, node.size, block.binary, track_numbers, track_numbers_len) == 0) return 0; if (block.binary.data) { int ret = OnBlock(cluster, block); if (ret != MKV_CONTINUE) return ret; } } break; default: nsmkv::ReadGlobal(stream, node.id, node.size); } } return MKV_CONTINUE; } int MKVPlayer::OnBlock(const nsmkv::Cluster &cluster, const nsmkv::Block &block) { if (WaitForSingleObject(killswitch, 0) == WAIT_TIMEOUT) { if (block.binary.track_number == audio_track_num) { return OnAudio(cluster, block.binary); } else if (block.binary.track_number == video_track_num) { return OnVideo(cluster, block.binary); } return MKV_CONTINUE; } else return MKV_ABORT; } int MKVPlayer::OnFirstCluster(uint64_t position) { if (video_decoder) { video_cluster_position = position; video_thread = CreateThread(0, 0, VideoThread, this, 0, 0); SetThreadPriority(video_thread, (int)AGAVE_API_CONFIG->GetInt(playbackConfigGroupGUID, L"priority", THREAD_PRIORITY_HIGHEST)); } return MKV_CONTINUE; } int MKVPlayer::Step(nsmkv::MKVReader *stream, uint64_t *track_numbers, size_t track_numbers_len) { uint64_t this_position = stream->Tell(); ebml_node node; if (read_ebml_node(stream, &node) == 0) return MKV_EOF; if (node.id != mkv_void) { nsmkv::SeekEntry seek_entry(node.id, this_position-segment_position); seek_table.AddEntry(seek_entry, nsmkv::SeekTable::ADDENTRY_FOUND); } switch(node.id) { case mkv_segment_segmentinfo: if (nsmkv::ReadSegmentInfo(stream, node.size, segment_info) == 0) return MKV_EOF; OnSegmentInfo(segment_info); break; case mkv_metaseek_seekhead: if (nsmkv::ReadSeekHead(stream, node.size, seek_table) == 0) return MKV_EOF; break; case mkv_segment_tracks: if (nsmkv::ReadTracks(stream, node.size, tracks) == 0) return MKV_EOF; return OnTracks(tracks); break; case mkv_segment_cues: if (!cues_searched) { if (nsmkv::ReadCues(stream, node.size, cues) == 0) return MKV_EOF; cues_searched=true; } else { stream->Skip(node.size); } break; case mkv_segment_cluster: if (!first_cluster_found) { first_cluster_found=true; OnFirstCluster(this_position); } return ParseCluster(stream, node.size, track_numbers, track_numbers_len); break; case mkv_segment_attachments: if (nsmkv::ReadAttachment(stream, node.size, attachments) == 0) return MKV_EOF; break; default: if (nsmkv::ReadGlobal(stream, node.id, node.size) == 0) return MKV_EOF; break; } return MKV_CONTINUE; } DWORD CALLBACK MKVPlayer::ThreadFunction() { // ===== tell audio output helper object about the output plugin ===== audio_output.Init(plugin.outMod); FILE *f = _wfopen(filename, L"rb"); if (!f) goto btfo; main_reader = new MKVReaderFILE(f); // ===== read the header ===== if (!ParseHeader()) goto btfo; // ===== find segment start ===== if (!FindSegment()) goto btfo; // TODO: try to find more segments? HANDLE handles[] = {killswitch, seek_event}; while(1) { int ret = WaitForMultipleObjects(2, handles, FALSE, 0); if (ret == WAIT_TIMEOUT) { int ret = Step(main_reader, &audio_track_num, 1); if (ret == MKV_EOF) { break; } else if (ret == MKV_ERROR || ret == MKV_STOP) { break; } } else if (ret == WAIT_OBJECT_0) // kill { break; } else if (ret == WAIT_OBJECT_0+1) // seek event { ResetEvent(seek_event); // pause video thread if (video_decoder) { SetEvent(video_break); WaitForSingleObject(video_flush_done, INFINITE); } FindCues(); uint64_t seek_time = segment_info.ConvertMillisecondsToTime(m_needseek); uint64_t curr_time = segment_info.ConvertMillisecondsToTime(plugin.outMod->GetOutputTime() + audio_first_timestamp); int direction = (curr_time < seek_time)?nsmkv::Cues::SEEK_FORWARD:nsmkv::Cues::SEEK_BACKWARD; nsmkv::CuePoint *cue_point = cues.GetCuePoint(seek_time, curr_time, direction); if (cue_point) { nsmkv::CueTrackPosition *position = cue_point->GetPosition(audio_track_num); if (!position) // some files don't have the audio track. we're going to assume the data is interleaved and just use the video track position = cue_point->GetPosition(video_track_num); if (position) { audio_flushing=FLUSH_SEEK; if (audio_decoder) audio_decoder->Flush(); main_reader->Seek(position->cluster_position + segment_position); } if (video_stream) { position = cue_point->GetPosition(video_track_num); if (position) { video_stream->Seek(position->cluster_position + segment_position); } } } else { // TODO enumerate clusters & blocks to find closest time (ugh) } if (video_decoder) { SetEvent(video_flush); WaitForSingleObject(video_flush_done, INFINITE); } } } delete main_reader; main_reader=0; if (WaitForSingleObject(killswitch, 0) != WAIT_OBJECT_0) { // TODO: tell audio decoder about end-of-stream and get remaining audio audio_output.Write(0,0); audio_output.WaitWhilePlaying(); if (WaitForSingleObject(killswitch, 0) != WAIT_OBJECT_0) PostMessage(plugin.hMainWindow, WM_WA_MPEG_EOF, 0, 0); } if (audio_decoder) { audio_decoder->Close(); audio_decoder=0; audio_output.Close(); } return 0; btfo: // bail the fuck out if (WaitForSingleObject(killswitch, 0) != WAIT_OBJECT_0) PostMessage(plugin.hMainWindow, WM_WA_MPEG_EOF, 0, 0); delete main_reader; main_reader=0; if (audio_decoder) { audio_decoder->Close(); audio_decoder=0; audio_output.Close(); } return 1; }