|
MythTV
0.26-pre
|
00001 // C headers 00002 #include <cerrno> 00003 #include <cassert> 00004 #include <cstring> 00005 #include <sys/types.h> 00006 #include <sys/stat.h> 00007 #include <unistd.h> 00008 00009 // C++ headers 00010 #include <algorithm> 00011 #include <iostream> 00012 using namespace std; 00013 00014 // Qt headers 00015 #include <QMutex> 00016 00017 // MythTV headers 00018 #include "mythconfig.h" 00019 #include "nuppeldecoder.h" 00020 #include "mythplayer.h" 00021 #include "remoteencoder.h" 00022 #include "mythlogging.h" 00023 #include "myth_imgconvert.h" 00024 #include "programinfo.h" 00025 00026 #include "minilzo.h" 00027 00028 extern "C" { 00029 #if HAVE_BIGENDIAN 00030 #include "bswap.h" 00031 #endif 00032 #include "libavutil/opt.h" 00033 } 00034 00035 #define LOC QString("NVD: ") 00036 00037 NuppelDecoder::NuppelDecoder(MythPlayer *parent, 00038 const ProgramInfo &pginfo) 00039 : DecoderBase(parent, pginfo), 00040 rtjd(0), video_width(0), video_height(0), video_size(0), 00041 video_frame_rate(0.0f), audio_samplerate(44100), 00042 #if HAVE_BIGENDIAN 00043 audio_bits_per_sample(0), 00044 #endif 00045 ffmpeg_extradatasize(0), ffmpeg_extradata(0), usingextradata(false), 00046 disablevideo(false), totalLength(0), totalFrames(0), effdsp(0), 00047 directframe(NULL), decoded_video_frame(NULL), 00048 mpa_vidcodec(0), mpa_vidctx(0), mpa_audcodec(0), mpa_audctx(0), 00049 directrendering(false), 00050 lastct('1'), strm(0), buf(0), buf2(0), 00051 videosizetotal(0), videoframesread(0), setreadahead(false) 00052 { 00053 // initialize structures 00054 memset(&fileheader, 0, sizeof(rtfileheader)); 00055 memset(&frameheader, 0, sizeof(rtframeheader)); 00056 memset(&extradata, 0, sizeof(extendeddata)); 00057 memset(&tmppicture, 0, sizeof(AVPicture)); 00058 planes[0] = planes[1] = planes[2] = 0; 00059 m_audioFrame = avcodec_alloc_frame(); 00060 00061 // set parent class variables 00062 positionMapType = MARK_KEYFRAME; 00063 lastKey = 0; 00064 framesPlayed = 0; 00065 getrawframes = false; 00066 getrawvideo = false; 00067 00068 rtjd = new RTjpeg(); 00069 int format = RTJ_YUV420; 00070 rtjd->SetFormat(&format); 00071 00072 { 00073 QMutexLocker locker(avcodeclock); 00074 avcodec_register_all(); 00075 } 00076 00077 if (lzo_init() != LZO_E_OK) 00078 { 00079 LOG(VB_GENERAL, LOG_ERR, "NuppelDecoder: lzo_init() failed, aborting"); 00080 errored = true; 00081 return; 00082 } 00083 } 00084 00085 NuppelDecoder::~NuppelDecoder() 00086 { 00087 if (rtjd) 00088 delete rtjd; 00089 if (ffmpeg_extradata) 00090 delete [] ffmpeg_extradata; 00091 if (buf) 00092 delete [] buf; 00093 if (buf2) 00094 delete [] buf2; 00095 if (strm_buf) 00096 delete [] strm_buf; 00097 00098 av_free(m_audioFrame); 00099 00100 while (!StoredData.empty()) 00101 { 00102 delete StoredData.front(); 00103 StoredData.pop_front(); 00104 } 00105 CloseAVCodecVideo(); 00106 CloseAVCodecAudio(); 00107 } 00108 00109 bool NuppelDecoder::CanHandle(char testbuf[kDecoderProbeBufferSize], 00110 int) 00111 { 00112 if (!strncmp(testbuf, "NuppelVideo", 11) || 00113 !strncmp(testbuf, "MythTVVideo", 11)) 00114 return true; 00115 return false; 00116 } 00117 00118 MythCodecID NuppelDecoder::GetVideoCodecID(void) const 00119 { 00120 MythCodecID value = kCodec_NONE; 00121 if (mpa_vidcodec) 00122 { 00123 if (QString(mpa_vidcodec->name) == "mpeg4") 00124 value = kCodec_NUV_MPEG4; 00125 } 00126 else if (usingextradata && extradata.video_fourcc == FOURCC_DIVX) 00127 value = kCodec_NUV_MPEG4; 00128 else 00129 value = kCodec_NUV_RTjpeg; 00130 return (value); 00131 } 00132 00133 QString NuppelDecoder::GetRawEncodingType(void) 00134 { 00135 if (mpa_vidctx) 00136 return ff_codec_id_string(mpa_vidctx->codec_id); 00137 return QString(); 00138 } 00139 00140 bool NuppelDecoder::ReadFileheader(struct rtfileheader *fh) 00141 { 00142 if (ringBuffer->Read(fh, FILEHEADERSIZE) != FILEHEADERSIZE) 00143 return false; 00144 00145 #if HAVE_BIGENDIAN 00146 fh->width = bswap_32(fh->width); 00147 fh->height = bswap_32(fh->height); 00148 fh->desiredwidth = bswap_32(fh->desiredwidth); 00149 fh->desiredheight = bswap_32(fh->desiredheight); 00150 fh->aspect = bswap_dbl(fh->aspect); 00151 fh->fps = bswap_dbl(fh->fps); 00152 fh->videoblocks = bswap_32(fh->videoblocks); 00153 fh->audioblocks = bswap_32(fh->audioblocks); 00154 fh->textsblocks = bswap_32(fh->textsblocks); 00155 fh->keyframedist = bswap_32(fh->keyframedist); 00156 #endif 00157 00158 return true; 00159 } 00160 00161 bool NuppelDecoder::ReadFrameheader(struct rtframeheader *fh) 00162 { 00163 if (ringBuffer->Read(fh, FRAMEHEADERSIZE) != FRAMEHEADERSIZE) 00164 return false; 00165 00166 #if HAVE_BIGENDIAN 00167 fh->timecode = bswap_32(fh->timecode); 00168 fh->packetlength = bswap_32(fh->packetlength); 00169 #endif 00170 00171 return true; 00172 } 00173 00174 int NuppelDecoder::OpenFile(RingBuffer *rbuffer, bool novideo, 00175 char testbuf[kDecoderProbeBufferSize], 00176 int) 00177 { 00178 (void)testbuf; 00179 00180 ringBuffer = rbuffer; 00181 disablevideo = novideo; 00182 tracks[kTrackTypeVideo].clear(); 00183 StreamInfo si(0, 0, 0, 0, 0); 00184 tracks[kTrackTypeVideo].push_back(si); 00185 selectedTrack[kTrackTypeVideo] = si; 00186 00187 struct rtframeheader frameheader; 00188 long long startpos = 0; 00189 int foundit = 0; 00190 char *space; 00191 00192 if (!ReadFileheader(&fileheader)) 00193 { 00194 LOG(VB_GENERAL, LOG_ERR, 00195 QString("Error reading file: %1").arg(ringBuffer->GetFilename())); 00196 return -1; 00197 } 00198 00199 while ((QString(fileheader.finfo) != "NuppelVideo") && 00200 (QString(fileheader.finfo) != "MythTVVideo")) 00201 { 00202 ringBuffer->Seek(startpos, SEEK_SET); 00203 char dummychar; 00204 ringBuffer->Read(&dummychar, 1); 00205 00206 startpos = ringBuffer->GetReadPosition(); 00207 00208 if (!ReadFileheader(&fileheader)) 00209 { 00210 LOG(VB_GENERAL, LOG_ERR, QString("Error reading file: %1") 00211 .arg(ringBuffer->GetFilename())); 00212 return -1; 00213 } 00214 00215 if (startpos > 20000) 00216 { 00217 LOG(VB_GENERAL, LOG_ERR, QString("Bad file: '%1'") 00218 .arg(ringBuffer->GetFilename())); 00219 return -1; 00220 } 00221 } 00222 00223 if (fileheader.aspect > .999 && fileheader.aspect < 1.001) 00224 fileheader.aspect = 4.0 / 3; 00225 current_aspect = fileheader.aspect; 00226 00227 GetPlayer()->SetKeyframeDistance(fileheader.keyframedist); 00228 GetPlayer()->SetVideoParams(fileheader.width, fileheader.height, 00229 fileheader.fps); 00230 00231 video_width = fileheader.width; 00232 video_height = fileheader.height; 00233 video_size = video_height * video_width * 3 / 2; 00234 keyframedist = fileheader.keyframedist; 00235 video_frame_rate = fileheader.fps; 00236 00237 if (!ReadFrameheader(&frameheader)) 00238 { 00239 LOG(VB_GENERAL, LOG_ERR, "File not big enough for a header"); 00240 return -1; 00241 } 00242 if (frameheader.frametype != 'D') 00243 { 00244 LOG(VB_GENERAL, LOG_ERR, "Illegal file format"); 00245 return -1; 00246 } 00247 00248 space = new char[video_size]; 00249 00250 if (frameheader.comptype == 'F') 00251 { 00252 ffmpeg_extradatasize = frameheader.packetlength; 00253 if (ffmpeg_extradatasize > 0) 00254 { 00255 ffmpeg_extradata = new uint8_t[ffmpeg_extradatasize]; 00256 if (frameheader.packetlength != ringBuffer->Read(ffmpeg_extradata, 00257 frameheader.packetlength)) 00258 { 00259 LOG(VB_GENERAL, LOG_ERR, 00260 "File not big enough for first frame data"); 00261 delete [] ffmpeg_extradata; 00262 ffmpeg_extradata = NULL; 00263 delete [] space; 00264 return -1; 00265 } 00266 } 00267 } 00268 else 00269 { 00270 if (frameheader.packetlength != ringBuffer->Read(space, 00271 frameheader.packetlength)) 00272 { 00273 LOG(VB_GENERAL, LOG_ERR, 00274 "File not big enough for first frame data"); 00275 delete [] space; 00276 return -1; 00277 } 00278 } 00279 00280 if ((video_height & 1) == 1) 00281 { 00282 video_height--; 00283 LOG(VB_GENERAL, LOG_ERR, 00284 QString("Incompatible video height, reducing to %1") 00285 .arg( video_height)); 00286 } 00287 00288 startpos = ringBuffer->GetReadPosition(); 00289 00290 ReadFrameheader(&frameheader); 00291 00292 if (frameheader.frametype == 'X') 00293 { 00294 if (frameheader.packetlength != EXTENDEDSIZE) 00295 { 00296 LOG(VB_GENERAL, LOG_ERR, "Corrupt file. Bad extended frame."); 00297 } 00298 else 00299 { 00300 ringBuffer->Read(&extradata, frameheader.packetlength); 00301 #if HAVE_BIGENDIAN 00302 struct extendeddata *ed = &extradata; 00303 ed->version = bswap_32(ed->version); 00304 ed->video_fourcc = bswap_32(ed->video_fourcc); 00305 ed->audio_fourcc = bswap_32(ed->audio_fourcc); 00306 ed->audio_sample_rate = bswap_32(ed->audio_sample_rate); 00307 ed->audio_bits_per_sample = bswap_32(ed->audio_bits_per_sample); 00308 ed->audio_channels = bswap_32(ed->audio_channels); 00309 ed->audio_compression_ratio = bswap_32(ed->audio_compression_ratio); 00310 ed->audio_quality = bswap_32(ed->audio_quality); 00311 ed->rtjpeg_quality = bswap_32(ed->rtjpeg_quality); 00312 ed->rtjpeg_luma_filter = bswap_32(ed->rtjpeg_luma_filter); 00313 ed->rtjpeg_chroma_filter = bswap_32(ed->rtjpeg_chroma_filter); 00314 ed->lavc_bitrate = bswap_32(ed->lavc_bitrate); 00315 ed->lavc_qmin = bswap_32(ed->lavc_qmin); 00316 ed->lavc_qmax = bswap_32(ed->lavc_qmax); 00317 ed->lavc_maxqdiff = bswap_32(ed->lavc_maxqdiff); 00318 ed->seektable_offset = bswap_64(ed->seektable_offset); 00319 ed->keyframeadjust_offset = bswap_64(ed->keyframeadjust_offset); 00320 #endif 00321 usingextradata = true; 00322 ReadFrameheader(&frameheader); 00323 } 00324 } 00325 00326 if (usingextradata && extradata.seektable_offset > 0) 00327 { 00328 long long currentpos = ringBuffer->GetReadPosition(); 00329 struct rtframeheader seek_frameheader; 00330 00331 int seekret = ringBuffer->Seek(extradata.seektable_offset, SEEK_SET); 00332 if (seekret == -1) 00333 { 00334 LOG(VB_GENERAL, LOG_ERR, 00335 QString("NuppelDecoder::OpenFile(): seek error (%1)") 00336 .arg(strerror(errno))); 00337 } 00338 00339 ReadFrameheader(&seek_frameheader); 00340 00341 if (seek_frameheader.frametype != 'Q') 00342 { 00343 LOG(VB_GENERAL, LOG_ERR, 00344 QString("Invalid seektable (frametype %1)") 00345 .arg((int)seek_frameheader.frametype)); 00346 } 00347 else 00348 { 00349 if (seek_frameheader.packetlength > 0) 00350 { 00351 char *seekbuf = new char[seek_frameheader.packetlength]; 00352 ringBuffer->Read(seekbuf, seek_frameheader.packetlength); 00353 00354 int numentries = seek_frameheader.packetlength / 00355 sizeof(struct seektable_entry); 00356 struct seektable_entry ste; 00357 int offset = 0; 00358 00359 ste.file_offset = 0; 00360 ste.keyframe_number = 0; 00361 00362 m_positionMapLock.lock(); 00363 00364 m_positionMap.clear(); 00365 m_positionMap.reserve(numentries); 00366 00367 for (int z = 0; z < numentries; z++) 00368 { 00369 memcpy(&ste, seekbuf + offset, 00370 sizeof(struct seektable_entry)); 00371 #if HAVE_BIGENDIAN 00372 ste.file_offset = bswap_64(ste.file_offset); 00373 ste.keyframe_number = bswap_32(ste.keyframe_number); 00374 #endif 00375 offset += sizeof(struct seektable_entry); 00376 00377 PosMapEntry e = {ste.keyframe_number, 00378 ste.keyframe_number * keyframedist, 00379 ste.file_offset}; 00380 m_positionMap.push_back(e); 00381 } 00382 hasFullPositionMap = true; 00383 totalLength = (int)((ste.keyframe_number * keyframedist * 1.0) / 00384 video_frame_rate); 00385 totalFrames = (long long)ste.keyframe_number * keyframedist; 00386 00387 m_positionMapLock.unlock(); 00388 00389 GetPlayer()->SetFileLength(totalLength, totalFrames); 00390 00391 delete [] seekbuf; 00392 } 00393 else 00394 LOG(VB_GENERAL, LOG_ERR, "0 length seek table"); 00395 } 00396 00397 ringBuffer->Seek(currentpos, SEEK_SET); 00398 } 00399 00400 if (usingextradata && extradata.keyframeadjust_offset > 0 && 00401 hasFullPositionMap) 00402 { 00403 long long currentpos = ringBuffer->GetReadPosition(); 00404 struct rtframeheader kfa_frameheader; 00405 00406 int kfa_ret = ringBuffer->Seek(extradata.keyframeadjust_offset, 00407 SEEK_SET); 00408 if (kfa_ret == -1) 00409 { 00410 LOG(VB_GENERAL, LOG_ERR, 00411 QString("NuppelDecoder::OpenFile(): keyframeadjust (%1)") 00412 .arg(strerror(errno))); 00413 } 00414 00415 ringBuffer->Read(&kfa_frameheader, FRAMEHEADERSIZE); 00416 00417 if (kfa_frameheader.frametype != 'K') 00418 { 00419 LOG(VB_GENERAL, LOG_ERR, 00420 QString("Invalid key frame adjust table (frametype %1)") 00421 .arg((int)kfa_frameheader.frametype)); 00422 } 00423 else 00424 { 00425 if (kfa_frameheader.packetlength > 0) 00426 { 00427 char *kfa_buf = new char[kfa_frameheader.packetlength]; 00428 ringBuffer->Read(kfa_buf, kfa_frameheader.packetlength); 00429 00430 int numentries = kfa_frameheader.packetlength / 00431 sizeof(struct kfatable_entry); 00432 struct kfatable_entry kfate; 00433 int offset = 0; 00434 int adjust = 0; 00435 QMap<long long, int> keyFrameAdjustMap; 00436 00437 for (int z = 0; z < numentries; z++) 00438 { 00439 memcpy(&kfate, kfa_buf + offset, 00440 sizeof(struct kfatable_entry)); 00441 #if HAVE_BIGENDIAN 00442 kfate.adjust = bswap_32(kfate.adjust); 00443 kfate.keyframe_number = bswap_32(kfate.keyframe_number); 00444 #endif 00445 offset += sizeof(struct kfatable_entry); 00446 00447 keyFrameAdjustMap[kfate.keyframe_number] = kfate.adjust; 00448 adjust += kfate.adjust; 00449 } 00450 hasKeyFrameAdjustTable = true; 00451 00452 totalLength -= (int)(adjust / video_frame_rate); 00453 totalFrames -= adjust; 00454 GetPlayer()->SetFileLength(totalLength, totalFrames); 00455 00456 adjust = 0; 00457 00458 { 00459 QMutexLocker locker(&m_positionMapLock); 00460 for (uint i = 0; i < m_positionMap.size(); i++) 00461 { 00462 long long adj = m_positionMap[i].adjFrame; 00463 00464 if (keyFrameAdjustMap.contains(adj)) 00465 adjust += keyFrameAdjustMap[adj]; 00466 00467 m_positionMap[i].adjFrame -= adjust; 00468 } 00469 } 00470 00471 delete [] kfa_buf; 00472 } 00473 else 00474 LOG(VB_GENERAL, LOG_ERR, "0 length key frame adjust table"); 00475 } 00476 00477 ringBuffer->Seek(currentpos, SEEK_SET); 00478 } 00479 00480 while (frameheader.frametype != 'A' && frameheader.frametype != 'V' && 00481 frameheader.frametype != 'S' && frameheader.frametype != 'T' && 00482 frameheader.frametype != 'R') 00483 { 00484 ringBuffer->Seek(startpos, SEEK_SET); 00485 00486 char dummychar; 00487 ringBuffer->Read(&dummychar, 1); 00488 00489 startpos = ringBuffer->GetReadPosition(); 00490 00491 if (!ReadFrameheader(&frameheader)) 00492 { 00493 delete [] space; 00494 return -1; 00495 } 00496 00497 if (startpos > 20000) 00498 { 00499 delete [] space; 00500 return -1; 00501 } 00502 } 00503 00504 foundit = 0; 00505 00506 effdsp = audio_samplerate * 100; 00507 m_audio->SetEffDsp(effdsp); 00508 00509 if (usingextradata) 00510 { 00511 effdsp = extradata.audio_sample_rate * 100; 00512 m_audio->SetEffDsp(effdsp); 00513 audio_samplerate = extradata.audio_sample_rate; 00514 #if HAVE_BIGENDIAN 00515 // Why only if using extradata? 00516 audio_bits_per_sample = extradata.audio_bits_per_sample; 00517 #endif 00518 AudioFormat format = FORMAT_NONE; 00519 switch (extradata.audio_bits_per_sample) 00520 { 00521 case 8: format = FORMAT_U8; break; 00522 case 16: format = FORMAT_S16; break; 00523 case 24: format = FORMAT_S24; break; 00524 case 32: format = FORMAT_S32; break; 00525 } 00526 00527 m_audio->SetAudioParams(format, extradata.audio_channels, 00528 extradata.audio_channels, 00529 CODEC_ID_NONE, extradata.audio_sample_rate, 00530 false /* AC3/DTS pass through */); 00531 m_audio->ReinitAudio(); 00532 foundit = 1; 00533 } 00534 00535 while (!foundit) 00536 { 00537 if (frameheader.frametype == 'S') 00538 { 00539 if (frameheader.comptype == 'A') 00540 { 00541 effdsp = frameheader.timecode; 00542 if (effdsp > 0) 00543 { 00544 m_audio->SetEffDsp(effdsp); 00545 foundit = 1; 00546 continue; 00547 } 00548 } 00549 } 00550 if (frameheader.frametype != 'R' && frameheader.packetlength != 0) 00551 { 00552 if (frameheader.packetlength != ringBuffer->Read(space, 00553 frameheader.packetlength)) 00554 { 00555 foundit = 1; 00556 continue; 00557 } 00558 } 00559 00560 long long startpos2 = ringBuffer->GetReadPosition(); 00561 00562 foundit = !ReadFrameheader(&frameheader); 00563 00564 bool framesearch = false; 00565 00566 while (frameheader.frametype != 'A' && frameheader.frametype != 'V' && 00567 frameheader.frametype != 'S' && frameheader.frametype != 'T' && 00568 frameheader.frametype != 'R' && frameheader.frametype != 'X') 00569 { 00570 if (!framesearch) 00571 LOG(VB_GENERAL, LOG_INFO, "Searching for frame header."); 00572 00573 framesearch = true; 00574 00575 ringBuffer->Seek(startpos2, SEEK_SET); 00576 00577 char dummychar; 00578 ringBuffer->Read(&dummychar, 1); 00579 00580 startpos2 = ringBuffer->GetReadPosition(); 00581 00582 foundit = !ReadFrameheader(&frameheader); 00583 if (foundit) 00584 break; 00585 } 00586 } 00587 00588 delete [] space; 00589 00590 setreadahead = false; 00591 00592 // mpeg4 encodes are small enough that this shouldn't matter 00593 if (usingextradata && extradata.video_fourcc == FOURCC_DIVX) 00594 setreadahead = true; 00595 00596 bitrate = 0; 00597 unsigned min_bitrate = 1000; 00598 if (usingextradata && extradata.video_fourcc == FOURCC_DIVX) 00599 { 00600 // Use video bitrate, ignore negligible audio bitrate 00601 bitrate = extradata.lavc_bitrate / 1000; 00602 } 00603 bitrate = max(bitrate, min_bitrate); // set minimum 1 Mb/s to be safe 00604 LOG(VB_PLAYBACK, LOG_INFO, 00605 QString("Setting bitrate to %1 Kb/s").arg(bitrate)); 00606 00607 ringBuffer->UpdateRawBitrate(GetRawBitrate()); 00608 00609 videosizetotal = 0; 00610 videoframesread = 0; 00611 00612 ringBuffer->Seek(startpos, SEEK_SET); 00613 00614 buf = new unsigned char[video_size]; 00615 strm_buf = new unsigned char[video_size * 2 + 16]; 00616 strm = (unsigned char*) (((long)strm_buf + 15) & ~0xf); 00617 00618 if (hasFullPositionMap) 00619 return 1; 00620 00621 if (SyncPositionMap()) 00622 return 1; 00623 00624 return 0; 00625 } 00626 00627 int get_nuppel_buffer(struct AVCodecContext *c, AVFrame *pic) 00628 { 00629 NuppelDecoder *nd = (NuppelDecoder *)(c->opaque); 00630 00631 int i; 00632 00633 for (i = 0; i < 3; i++) 00634 { 00635 pic->data[i] = nd->directframe->buf + nd->directframe->offsets[i]; 00636 pic->linesize[i] = nd->directframe->pitches[i]; 00637 } 00638 00639 pic->opaque = nd->directframe; 00640 pic->type = FF_BUFFER_TYPE_USER; 00641 00642 return 1; 00643 } 00644 00645 void release_nuppel_buffer(struct AVCodecContext *c, AVFrame *pic) 00646 { 00647 (void)c; 00648 assert(pic->type == FF_BUFFER_TYPE_USER); 00649 00650 NuppelDecoder *nd = (NuppelDecoder *)(c->opaque); 00651 if (nd && nd->GetPlayer()) 00652 nd->GetPlayer()->DeLimboFrame((VideoFrame*)pic->opaque); 00653 00654 int i; 00655 for (i = 0; i < 4; i++) 00656 pic->data[i] = NULL; 00657 } 00658 00659 bool NuppelDecoder::InitAVCodecVideo(int codec) 00660 { 00661 if (mpa_vidcodec) 00662 CloseAVCodecVideo(); 00663 00664 if (usingextradata) 00665 { 00666 switch(extradata.video_fourcc) 00667 { 00668 case FOURCC_DIVX: codec = CODEC_ID_MPEG4; break; 00669 case FOURCC_WMV1: codec = CODEC_ID_WMV1; break; 00670 case FOURCC_DIV3: codec = CODEC_ID_MSMPEG4V3; break; 00671 case FOURCC_MP42: codec = CODEC_ID_MSMPEG4V2; break; 00672 case FOURCC_MPG4: codec = CODEC_ID_MSMPEG4V1; break; 00673 case FOURCC_MJPG: codec = CODEC_ID_MJPEG; break; 00674 case FOURCC_H263: codec = CODEC_ID_H263; break; 00675 case FOURCC_H264: codec = CODEC_ID_H264; break; 00676 case FOURCC_I263: codec = CODEC_ID_H263I; break; 00677 case FOURCC_MPEG: codec = CODEC_ID_MPEG1VIDEO; break; 00678 case FOURCC_MPG2: codec = CODEC_ID_MPEG2VIDEO; break; 00679 case FOURCC_HFYU: codec = CODEC_ID_HUFFYUV; break; 00680 default: codec = -1; 00681 } 00682 } 00683 mpa_vidcodec = avcodec_find_decoder((enum CodecID)codec); 00684 00685 if (!mpa_vidcodec) 00686 { 00687 if (usingextradata) 00688 LOG(VB_GENERAL, LOG_ERR, 00689 QString("couldn't find video codec (%1)") 00690 .arg(extradata.video_fourcc)); 00691 else 00692 LOG(VB_GENERAL, LOG_ERR, "couldn't find video codec"); 00693 return false; 00694 } 00695 00696 if (mpa_vidcodec->capabilities & CODEC_CAP_DR1 && codec != CODEC_ID_MJPEG) 00697 directrendering = true; 00698 00699 if (mpa_vidctx) 00700 av_free(mpa_vidctx); 00701 00702 mpa_vidctx = avcodec_alloc_context3(NULL); 00703 00704 mpa_vidctx->codec_id = (enum CodecID)codec; 00705 mpa_vidctx->codec_type = AVMEDIA_TYPE_VIDEO; 00706 mpa_vidctx->width = video_width; 00707 mpa_vidctx->height = video_height; 00708 mpa_vidctx->err_recognition = AV_EF_CRCCHECK | AV_EF_BITSTREAM | 00709 AV_EF_BUFFER; 00710 mpa_vidctx->bits_per_coded_sample = 12; 00711 00712 if (directrendering) 00713 { 00714 mpa_vidctx->flags |= CODEC_FLAG_EMU_EDGE; 00715 mpa_vidctx->draw_horiz_band = NULL; 00716 mpa_vidctx->get_buffer = get_nuppel_buffer; 00717 mpa_vidctx->release_buffer = release_nuppel_buffer; 00718 mpa_vidctx->opaque = (void *)this; 00719 } 00720 if (ffmpeg_extradatasize > 0) 00721 { 00722 av_opt_set_int(mpa_vidctx, "extern_huff", 1, 0); 00723 mpa_vidctx->extradata = ffmpeg_extradata; 00724 mpa_vidctx->extradata_size = ffmpeg_extradatasize; 00725 } 00726 00727 QMutexLocker locker(avcodeclock); 00728 if (avcodec_open2(mpa_vidctx, mpa_vidcodec, NULL) < 0) 00729 { 00730 LOG(VB_GENERAL, LOG_ERR, LOC + "Couldn't find lavc video codec"); 00731 return false; 00732 } 00733 00734 return true; 00735 } 00736 00737 void NuppelDecoder::CloseAVCodecVideo(void) 00738 { 00739 QMutexLocker locker(avcodeclock); 00740 00741 if (mpa_vidcodec) 00742 { 00743 avcodec_close(mpa_vidctx); 00744 00745 if (mpa_vidctx) 00746 { 00747 av_free(mpa_vidctx); 00748 mpa_vidctx = NULL; 00749 } 00750 } 00751 } 00752 00753 bool NuppelDecoder::InitAVCodecAudio(int codec) 00754 { 00755 if (mpa_audcodec) 00756 CloseAVCodecAudio(); 00757 00758 if (usingextradata) 00759 { 00760 switch(extradata.audio_fourcc) 00761 { 00762 case FOURCC_LAME: codec = CODEC_ID_MP3; break; 00763 case FOURCC_AC3 : codec = CODEC_ID_AC3; break; 00764 default: codec = -1; 00765 } 00766 } 00767 mpa_audcodec = avcodec_find_decoder((enum CodecID)codec); 00768 00769 if (!mpa_audcodec) 00770 { 00771 if (usingextradata) 00772 LOG(VB_GENERAL, LOG_ERR, QString("couldn't find audio codec (%1)") 00773 .arg(extradata.audio_fourcc)); 00774 else 00775 LOG(VB_GENERAL, LOG_ERR, "couldn't find audio codec"); 00776 return false; 00777 } 00778 00779 if (mpa_audctx) 00780 av_free(mpa_audctx); 00781 00782 mpa_audctx = avcodec_alloc_context3(NULL); 00783 00784 mpa_audctx->codec_id = (enum CodecID)codec; 00785 mpa_audctx->codec_type = AVMEDIA_TYPE_AUDIO; 00786 00787 QMutexLocker locker(avcodeclock); 00788 if (avcodec_open2(mpa_audctx, mpa_audcodec, NULL) < 0) 00789 { 00790 LOG(VB_GENERAL, LOG_ERR, LOC + "Couldn't find lavc audio codec"); 00791 return false; 00792 } 00793 00794 return true; 00795 } 00796 00797 void NuppelDecoder::CloseAVCodecAudio(void) 00798 { 00799 QMutexLocker locker(avcodeclock); 00800 00801 if (mpa_audcodec) 00802 { 00803 avcodec_close(mpa_audctx); 00804 00805 if (mpa_audctx) 00806 { 00807 av_free(mpa_audctx); 00808 mpa_audctx = NULL; 00809 } 00810 } 00811 } 00812 00813 static void CopyToVideo(unsigned char *buf, int video_width, 00814 int video_height, VideoFrame *frame) 00815 { 00816 uint ysize = video_width * video_height; 00817 uint uvsize = ysize >> 2; 00818 00819 unsigned char *planes[3]; 00820 planes[0] = buf; 00821 planes[1] = planes[0] + ysize; 00822 planes[2] = planes[1] + uvsize; 00823 00824 memcpy(frame->buf + frame->offsets[0], planes[0], ysize); 00825 memcpy(frame->buf + frame->offsets[1], planes[1], uvsize); 00826 memcpy(frame->buf + frame->offsets[2], planes[2], uvsize); 00827 } 00828 00829 bool NuppelDecoder::DecodeFrame(struct rtframeheader *frameheader, 00830 unsigned char *lstrm, VideoFrame *frame) 00831 { 00832 int r; 00833 lzo_uint out_len; 00834 int compoff = 0; 00835 AVPacket pkt; 00836 00837 unsigned char *outbuf = frame->buf; 00838 directframe = frame; 00839 00840 if (!buf2) 00841 { 00842 buf2 = new unsigned char[video_size + 64]; 00843 planes[0] = buf; 00844 planes[1] = planes[0] + video_width * video_height; 00845 planes[2] = planes[1] + (video_width * video_height) / 4; 00846 } 00847 00848 if (frameheader->comptype == 'N') { 00849 memset(outbuf, 0, video_width * video_height); 00850 memset(outbuf + video_width * video_height, 127, 00851 (video_width * video_height)/2); 00852 return true; 00853 } 00854 00855 if (frameheader->comptype == 'L') { 00856 switch(lastct) { 00857 case '0': case '3': 00858 CopyToVideo(buf2, video_width, video_height, frame); 00859 break; 00860 case '1': case '2': 00861 default: 00862 CopyToVideo(buf, video_width, video_height, frame); 00863 break; 00864 } 00865 return true; 00866 } 00867 00868 compoff = 1; 00869 if (frameheader->comptype == '2' || frameheader->comptype == '3') 00870 compoff=0; 00871 00872 lastct = frameheader->comptype; 00873 00874 if (!compoff) 00875 { 00876 r = lzo1x_decompress(lstrm, frameheader->packetlength, buf2, &out_len, 00877 NULL); 00878 if (r != LZO_E_OK) 00879 { 00880 LOG(VB_GENERAL, LOG_ERR, "minilzo: can't decompress illegal data"); 00881 } 00882 } 00883 00884 if (frameheader->comptype == '0') 00885 { 00886 CopyToVideo(lstrm, video_width, video_height, frame); 00887 return true; 00888 } 00889 00890 if (frameheader->comptype == '3') 00891 { 00892 CopyToVideo(buf2, video_width, video_height, frame); 00893 return true; 00894 } 00895 00896 if (frameheader->comptype == '2' || frameheader->comptype == '1') 00897 { 00898 if (compoff) 00899 rtjd->Decompress((int8_t*)lstrm, planes); 00900 else 00901 rtjd->Decompress((int8_t*)buf2, planes); 00902 00903 CopyToVideo(buf, video_width, video_height, frame); 00904 } 00905 else 00906 { 00907 if (!mpa_vidcodec) 00908 InitAVCodecVideo(frameheader->comptype - '3'); 00909 00910 AVFrame mpa_pic; 00911 av_init_packet(&pkt); 00912 pkt.data = lstrm; 00913 pkt.size = frameheader->packetlength; 00914 00915 { 00916 QMutexLocker locker(avcodeclock); 00917 // if directrendering, writes into buf 00918 int gotpicture = 0; 00919 int ret = avcodec_decode_video2(mpa_vidctx, &mpa_pic, &gotpicture, 00920 &pkt); 00921 directframe = NULL; 00922 if (ret < 0) 00923 { 00924 LOG(VB_PLAYBACK, LOG_ERR, LOC + 00925 QString("avcodec_decode_video returned: %1").arg(ret)); 00926 return false; 00927 } 00928 else if (!gotpicture) 00929 { 00930 return false; 00931 } 00932 } 00933 00934 /* XXX: Broken 00935 if (mpa_pic->qscale_table != NULL && mpa_pic->qstride > 0) 00936 { 00937 int tablesize = mpa_pic->qstride * ((video_height + 15) / 16); 00938 00939 if (frame->qstride != mpa_pic->qstride || 00940 frame->qscale_table == NULL) 00941 { 00942 frame->qstride = mpa_pic->qstride; 00943 00944 if (frame->qscale_table) 00945 delete [] frame->qscale_table; 00946 00947 frame->qscale_table = new unsigned char[tablesize]; 00948 } 00949 00950 memcpy(frame->qscale_table, mpa_pic->qscale_table, tablesize); 00951 } 00952 */ 00953 00954 if (directrendering) 00955 return true; 00956 00957 avpicture_fill(&tmppicture, outbuf, PIX_FMT_YUV420P, video_width, 00958 video_height); 00959 00960 myth_sws_img_convert( 00961 &tmppicture, PIX_FMT_YUV420P, (AVPicture *)&mpa_pic, 00962 mpa_vidctx->pix_fmt, video_width, video_height); 00963 } 00964 00965 return true; 00966 } 00967 00968 bool NuppelDecoder::isValidFrametype(char type) 00969 { 00970 switch (type) 00971 { 00972 case 'A': case 'V': case 'S': case 'T': case 'R': case 'X': 00973 case 'M': case 'D': case 'Q': case 'K': 00974 return true; 00975 default: 00976 return false; 00977 } 00978 00979 return false; 00980 } 00981 00982 void NuppelDecoder::StoreRawData(unsigned char *newstrm) 00983 { 00984 unsigned char *strmcpy; 00985 if (newstrm) 00986 { 00987 strmcpy = new unsigned char[frameheader.packetlength]; 00988 memcpy(strmcpy, newstrm, frameheader.packetlength); 00989 } 00990 else 00991 strmcpy = NULL; 00992 00993 StoredData.push_back(new RawDataList(frameheader, strmcpy)); 00994 } 00995 00996 // The return value is the number of bytes in StoredData before the 'SV' frame 00997 long NuppelDecoder::UpdateStoredFrameNum(long framenum) 00998 { 00999 long sync_offset = 0; 01000 01001 list<RawDataList*>::iterator it = StoredData.begin(); 01002 for ( ; it != StoredData.end(); ++it) 01003 { 01004 RawDataList *data = *it; 01005 if (data->frameheader.frametype == 'S' && 01006 data->frameheader.comptype == 'V') 01007 { 01008 data->frameheader.timecode = framenum; 01009 return sync_offset; 01010 } 01011 sync_offset += FRAMEHEADERSIZE; 01012 if (data->packet) 01013 sync_offset += data->frameheader.packetlength; 01014 } 01015 return 0; 01016 } 01017 01018 void NuppelDecoder::WriteStoredData(RingBuffer *rb, bool storevid, 01019 long timecodeOffset) 01020 { 01021 RawDataList *data; 01022 while (!StoredData.empty()) 01023 { 01024 data = StoredData.front(); 01025 01026 if (data->frameheader.frametype != 'S') 01027 data->frameheader.timecode -= timecodeOffset; 01028 01029 if (storevid || data->frameheader.frametype != 'V') 01030 { 01031 rb->Write(&(data->frameheader), FRAMEHEADERSIZE); 01032 if (data->packet) 01033 rb->Write(data->packet, data->frameheader.packetlength); 01034 } 01035 StoredData.pop_front(); 01036 delete data; 01037 } 01038 } 01039 01040 void NuppelDecoder::ClearStoredData() 01041 { 01042 RawDataList *data; 01043 while (!StoredData.empty()) 01044 { 01045 data = StoredData.front(); 01046 StoredData.pop_front(); 01047 delete data; 01048 } 01049 } 01050 01051 bool NuppelDecoder::GetFrame(DecodeType decodetype) 01052 { 01053 bool gotvideo = false; 01054 bool ret = false; 01055 int seeklen = 0; 01056 AVPacket pkt; 01057 01058 decoded_video_frame = NULL; 01059 01060 while (!gotvideo) 01061 { 01062 long long currentposition = ringBuffer->GetReadPosition(); 01063 if (waitingForChange && currentposition + 4 >= readAdjust) 01064 { 01065 FileChanged(); 01066 currentposition = ringBuffer->GetReadPosition(); 01067 } 01068 01069 if (!ReadFrameheader(&frameheader)) 01070 { 01071 SetEof(true); 01072 return false; 01073 } 01074 01075 01076 if (!ringBuffer->LiveMode() && 01077 ((frameheader.frametype == 'Q') || (frameheader.frametype == 'K'))) 01078 { 01079 SetEof(true); 01080 return false; 01081 } 01082 01083 bool framesearch = false; 01084 01085 while (!isValidFrametype(frameheader.frametype)) 01086 { 01087 if (!framesearch) 01088 LOG(VB_GENERAL, LOG_INFO, "Searching for frame header."); 01089 01090 framesearch = true; 01091 01092 ringBuffer->Seek((long long)seeklen-FRAMEHEADERSIZE, SEEK_CUR); 01093 01094 if (!ReadFrameheader(&frameheader)) 01095 { 01096 SetEof(true); 01097 return false; 01098 } 01099 seeklen = 1; 01100 } 01101 01102 if (frameheader.frametype == 'M') 01103 { 01104 int sizetoskip = sizeof(rtfileheader) - sizeof(rtframeheader); 01105 char *dummy = new char[sizetoskip + 1]; 01106 01107 if (ringBuffer->Read(dummy, sizetoskip) != sizetoskip) 01108 { 01109 delete [] dummy; 01110 SetEof(true); 01111 return false; 01112 } 01113 01114 delete [] dummy; 01115 continue; 01116 } 01117 01118 if (frameheader.frametype == 'R') 01119 { 01120 if (getrawframes) 01121 StoreRawData(NULL); 01122 continue; // the R-frame has no data packet 01123 } 01124 01125 if (frameheader.frametype == 'S') 01126 { 01127 if (frameheader.comptype == 'A') 01128 { 01129 if (frameheader.timecode > 2000000 && 01130 frameheader.timecode < 5500000) 01131 { 01132 effdsp = frameheader.timecode; 01133 m_audio->SetEffDsp(effdsp); 01134 } 01135 } 01136 else if (frameheader.comptype == 'V') 01137 { 01138 lastKey = frameheader.timecode; 01139 framesPlayed = (frameheader.timecode > 0 ? 01140 frameheader.timecode - 1 : 0); 01141 01142 if (!hasFullPositionMap) 01143 { 01144 long long last_index = 0; 01145 long long this_index = lastKey / keyframedist; 01146 01147 QMutexLocker locker(&m_positionMapLock); 01148 if (!m_positionMap.empty()) 01149 last_index = m_positionMap.back().index; 01150 01151 if (this_index > last_index) 01152 { 01153 PosMapEntry e = {this_index, lastKey, currentposition}; 01154 m_positionMap.push_back(e); 01155 } 01156 } 01157 } 01158 if (getrawframes) 01159 StoreRawData(NULL); 01160 } 01161 01162 if (frameheader.packetlength > 0) 01163 { 01164 if (frameheader.packetlength > 10485760) // arbitrary 10MB limit 01165 { 01166 LOG(VB_GENERAL, LOG_ERR, QString("Broken packet: %1 %2") 01167 .arg(frameheader.frametype) 01168 .arg(frameheader.packetlength)); 01169 SetEof(true); 01170 return false; 01171 } 01172 if (ringBuffer->Read(strm, frameheader.packetlength) != 01173 frameheader.packetlength) 01174 { 01175 SetEof(true); 01176 return false; 01177 } 01178 } 01179 else 01180 continue; 01181 01182 if (frameheader.frametype == 'V') 01183 { 01184 if (!(kDecodeVideo & decodetype)) 01185 { 01186 framesPlayed++; 01187 gotvideo = 1; 01188 continue; 01189 } 01190 01191 VideoFrame *buf = GetPlayer()->GetNextVideoFrame(); 01192 if (!buf) 01193 continue; 01194 01195 ret = DecodeFrame(&frameheader, strm, buf); 01196 if (!ret) 01197 { 01198 GetPlayer()->DiscardVideoFrame(buf); 01199 continue; 01200 } 01201 01202 buf->aspect = current_aspect; 01203 buf->frameNumber = framesPlayed; 01204 buf->dummy = 0; 01205 GetPlayer()->ReleaseNextVideoFrame(buf, frameheader.timecode); 01206 01207 // We need to make the frame available ourselves 01208 // if we are not using ffmpeg/avlib. 01209 if (directframe) 01210 GetPlayer()->DeLimboFrame(buf); 01211 01212 decoded_video_frame = buf; 01213 gotvideo = 1; 01214 if (getrawframes && getrawvideo) 01215 StoreRawData(strm); 01216 framesPlayed++; 01217 01218 if (!setreadahead) 01219 { 01220 videosizetotal += frameheader.packetlength; 01221 videoframesread++; 01222 01223 if (videoframesread > 15) 01224 { 01225 videosizetotal /= videoframesread; 01226 01227 float bps = (videosizetotal * 8.0f / 1024.0f * 01228 video_frame_rate); 01229 bitrate = (uint) (bps * 1.5f); 01230 01231 ringBuffer->UpdateRawBitrate(GetRawBitrate()); 01232 setreadahead = true; 01233 } 01234 } 01235 continue; 01236 } 01237 01238 if (frameheader.frametype=='A' && (kDecodeAudio & decodetype)) 01239 { 01240 if ((frameheader.comptype == '3') || (frameheader.comptype == 'A')) 01241 { 01242 if (getrawframes) 01243 StoreRawData(strm); 01244 01245 if (!mpa_audcodec) 01246 { 01247 if (frameheader.comptype == '3') 01248 InitAVCodecAudio(CODEC_ID_MP3); 01249 else if (frameheader.comptype == 'A') 01250 InitAVCodecAudio(CODEC_ID_AC3); 01251 else 01252 { 01253 LOG(VB_GENERAL, LOG_ERR, LOC + QString("GetFrame: " 01254 "Unknown audio comptype of '%1', skipping") 01255 .arg(frameheader.comptype)); 01256 return false; 01257 } 01258 } 01259 01260 av_init_packet(&pkt); 01261 pkt.data = strm; 01262 pkt.size = frameheader.packetlength; 01263 int ret = 0; 01264 01265 QMutexLocker locker(avcodeclock); 01266 01267 while (pkt.size > 0) 01268 { 01269 int got_frame = 0; 01270 ret = avcodec_decode_audio4(mpa_audctx, m_audioFrame, 01271 &got_frame, &pkt); 01272 01273 if (got_frame && ret > 0) 01274 { 01275 int data_size = 01276 av_samples_get_buffer_size(NULL, 01277 mpa_audctx->channels, 01278 m_audioFrame->nb_samples, 01279 mpa_audctx->sample_fmt, 01280 1); 01281 m_audio->AddAudioData( 01282 (char *)m_audioFrame->extended_data[0], 01283 data_size, frameheader.timecode, 0); 01284 } 01285 01286 pkt.size -= ret; 01287 pkt.data += ret; 01288 } 01289 } 01290 else 01291 { 01292 getrawframes = 0; 01293 #if HAVE_BIGENDIAN 01294 // Why endian correct the audio buffer here? 01295 // Don't big-endian clients have to do it in audiooutBlah.cpp? 01296 if (audio_bits_per_sample == 16) { 01297 // swap bytes 01298 for (int i = 0; i < (frameheader.packetlength & ~1); i+=2) { 01299 char tmp; 01300 tmp = strm[i+1]; 01301 strm[i+1] = strm[i]; 01302 strm[i] = tmp; 01303 } 01304 } 01305 #endif 01306 LOG(VB_PLAYBACK, LOG_DEBUG, QString("A audio timecode %1") 01307 .arg(frameheader.timecode)); 01308 m_audio->AddAudioData((char *)strm, frameheader.packetlength, 01309 frameheader.timecode, 0); 01310 } 01311 } 01312 01313 if (frameheader.frametype == 'T' && (kDecodeVideo & decodetype)) 01314 { 01315 if (getrawframes) 01316 StoreRawData(strm); 01317 01318 GetPlayer()->GetCC608Reader()->AddTextData(strm, frameheader.packetlength, 01319 frameheader.timecode, frameheader.comptype); 01320 } 01321 01322 if (frameheader.frametype == 'S' && frameheader.comptype == 'M') 01323 { 01324 unsigned char *eop = strm + frameheader.packetlength; 01325 unsigned char *cur = strm; 01326 01327 struct rtfileheader tmphead; 01328 struct rtfileheader *fh = &tmphead; 01329 01330 memcpy(fh, cur, min((int)sizeof(*fh), frameheader.packetlength)); 01331 01332 while (QString(fh->finfo) != "MythTVVideo" && 01333 cur + frameheader.packetlength <= eop) 01334 { 01335 cur++; 01336 memcpy(fh, cur, min((int)sizeof(*fh), frameheader.packetlength)); 01337 } 01338 01339 if (QString(fh->finfo) == "MythTVVideo") 01340 { 01341 #if HAVE_BIGENDIAN 01342 fh->width = bswap_32(fh->width); 01343 fh->height = bswap_32(fh->height); 01344 fh->desiredwidth = bswap_32(fh->desiredwidth); 01345 fh->desiredheight = bswap_32(fh->desiredheight); 01346 fh->aspect = bswap_dbl(fh->aspect); 01347 fh->fps = bswap_dbl(fh->fps); 01348 fh->videoblocks = bswap_32(fh->videoblocks); 01349 fh->audioblocks = bswap_32(fh->audioblocks); 01350 fh->textsblocks = bswap_32(fh->textsblocks); 01351 fh->keyframedist = bswap_32(fh->keyframedist); 01352 #endif 01353 01354 fileheader = *fh; 01355 01356 if (fileheader.aspect > .999 && fileheader.aspect < 1.001) 01357 fileheader.aspect = 4.0 / 3; 01358 current_aspect = fileheader.aspect; 01359 01360 GetPlayer()->SetKeyframeDistance(fileheader.keyframedist); 01361 GetPlayer()->SetVideoParams(fileheader.width, fileheader.height, 01362 fileheader.fps); 01363 } 01364 } 01365 } 01366 01367 framesRead = framesPlayed; 01368 01369 return true; 01370 } 01371 01372 void NuppelDecoder::SeekReset(long long newKey, uint skipFrames, 01373 bool doFlush, bool discardFrames) 01374 { 01375 LOG(VB_PLAYBACK, LOG_INFO, LOC + 01376 QString("SeekReset(%1, %2, %3 flush, %4 discard)") 01377 .arg(newKey).arg(skipFrames) 01378 .arg((doFlush) ? "do" : "don't") 01379 .arg((discardFrames) ? "do" : "don't")); 01380 01381 DecoderBase::SeekReset(newKey, skipFrames, doFlush, discardFrames); 01382 01383 if (mpa_vidcodec && doFlush) 01384 avcodec_flush_buffers(mpa_vidctx); 01385 01386 if (discardFrames) 01387 GetPlayer()->DiscardVideoFrames(doFlush); 01388 01389 for (;(skipFrames > 0) && !ateof; skipFrames--) 01390 { 01391 GetFrame(kDecodeAV); 01392 if (decoded_video_frame) 01393 GetPlayer()->DiscardVideoFrame(decoded_video_frame); 01394 } 01395 } 01396 01397 /* vim: set expandtab tabstop=4 shiftwidth=4: */
1.7.6.1