1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim:set ts=2 sw=2 sts=2 et cindent: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 #if !defined(OggCodecState_h_) 7 #define OggCodecState_h_ 8 9 #include <ogg/ogg.h> 10 // For MOZ_SAMPLE_TYPE_* 11 #include <nsAutoPtr.h> 12 #include <nsAutoRef.h> 13 #include <nsDeque.h> 14 #include <nsTArray.h> 15 #include <nsClassHashtable.h> 16 #include "VideoUtils.h" 17 #include "FlacFrameParser.h" 18 19 #include <theora/theoradec.h> 20 #ifdef MOZ_TREMOR 21 #include <tremor/ivorbiscodec.h> 22 #else 23 #include <vorbis/codec.h> 24 #endif 25 26 // Uncomment the following to validate that we're predicting the number 27 // of Vorbis samples in each packet correctly. 28 #define VALIDATE_VORBIS_SAMPLE_CALCULATION 29 #ifdef VALIDATE_VORBIS_SAMPLE_CALCULATION 30 #include <map> 31 #endif 32 33 struct OpusMSDecoder; 34 35 namespace mozilla { 36 37 class OpusParser; 38 39 // Deallocates a packet, used in OggPacketQueue below. 40 class OggPacketDeallocator : public nsDequeFunctor 41 { operator()42 virtual void* operator() (void* aPacket) 43 { 44 ogg_packet* p = static_cast<ogg_packet*>(aPacket); 45 delete [] p->packet; 46 delete p; 47 return nullptr; 48 } 49 }; 50 51 // A queue of ogg_packets. When we read a page, we extract the page's packets 52 // and buffer them in the owning stream's OggCodecState. This is because 53 // if we're skipping up to the next keyframe in very large frame sized videos, 54 // there may be several megabytes of data between keyframes, and the 55 // ogg_stream_state would end up resizing its buffer every time we added a 56 // new 4KB page to the bitstream, which kills performance on Windows. This 57 // also gives us the option to timestamp packets rather than decoded 58 // frames/samples, reducing the amount of frames/samples we must decode to 59 // determine start-time at a particular offset, and gives us finer control 60 // over memory usage. 61 class OggPacketQueue : private nsDeque 62 { 63 public: OggPacketQueue()64 OggPacketQueue() : nsDeque(new OggPacketDeallocator()) {} ~OggPacketQueue()65 ~OggPacketQueue() { Erase(); } IsEmpty()66 bool IsEmpty() { return nsDeque::GetSize() == 0; } 67 void Append(ogg_packet* aPacket); PopFront()68 ogg_packet* PopFront() { return static_cast<ogg_packet*>(nsDeque::PopFront()); } PeekFront()69 ogg_packet* PeekFront() { return static_cast<ogg_packet*>(nsDeque::PeekFront()); } Pop()70 ogg_packet* Pop() { return static_cast<ogg_packet*>(nsDeque::Pop()); } PushFront(ogg_packet * aPacket)71 void PushFront(ogg_packet* aPacket) { nsDeque::PushFront(aPacket); } Erase()72 void Erase() { nsDeque::Erase(); } 73 }; 74 75 // Encapsulates the data required for decoding an ogg bitstream and for 76 // converting granulepos to timestamps. 77 class OggCodecState 78 { 79 public: 80 typedef mozilla::MetadataTags MetadataTags; 81 // Ogg types we know about 82 enum CodecType 83 { 84 TYPE_VORBIS=0, 85 TYPE_THEORA, 86 TYPE_OPUS, 87 TYPE_SKELETON, 88 TYPE_FLAC, 89 TYPE_UNKNOWN 90 }; 91 92 virtual ~OggCodecState(); 93 94 // Factory for creating nsCodecStates. Use instead of constructor. 95 // aPage should be a beginning-of-stream page. 96 static OggCodecState* Create(ogg_page* aPage); 97 GetType()98 virtual CodecType GetType() { return TYPE_UNKNOWN; } 99 100 // Reads a header packet. Returns false if an error was encountered 101 // while reading header packets. Callers should check DoneReadingHeaders() 102 // to determine if the last header has been read. 103 // This function takes ownership of the packet and is responsible for 104 // releasing it or queuing it for later processing. DecodeHeader(ogg_packet * aPacket)105 virtual bool DecodeHeader(ogg_packet* aPacket) 106 { 107 return (mDoneReadingHeaders = true); 108 } 109 110 // Build a hash table with tag metadata parsed from the stream. GetTags()111 virtual MetadataTags* GetTags() 112 { 113 return nullptr; 114 } 115 116 // Returns the end time that a granulepos represents. Time(int64_t granulepos)117 virtual int64_t Time(int64_t granulepos) { return -1; } 118 119 // Returns the start time that a granulepos represents. StartTime(int64_t granulepos)120 virtual int64_t StartTime(int64_t granulepos) { return -1; } 121 122 // Returns the duration of the given packet, if it can be determined. PacketDuration(ogg_packet * aPacket)123 virtual int64_t PacketDuration(ogg_packet* aPacket) { return -1; } 124 125 // Returns the start time of the given packet, if it can be determined. PacketStartTime(ogg_packet * aPacket)126 virtual int64_t PacketStartTime(ogg_packet* aPacket) 127 { 128 if (aPacket->granulepos < 0) { 129 return -1; 130 } 131 int64_t endTime = Time(aPacket->granulepos); 132 int64_t duration = PacketDuration(aPacket); 133 if (duration > endTime) { 134 // Audio preskip may eat a whole packet or more. 135 return 0; 136 } else { 137 return endTime - duration; 138 } 139 } 140 141 // Initializes the codec state. Init()142 virtual bool Init() { return true; } 143 144 // Returns true when this bitstream has finished reading all its 145 // header packets. DoneReadingHeaders()146 bool DoneReadingHeaders() { return mDoneReadingHeaders; } 147 148 // Deactivates the bitstream. Only the primary video and audio bitstreams 149 // should be active. Deactivate()150 void Deactivate() 151 { 152 mActive = false; 153 mDoneReadingHeaders = true; 154 Reset(); 155 } 156 157 // Resets decoding state. 158 virtual nsresult Reset(); 159 160 // Returns true if the OggCodecState thinks this packet is a header 161 // packet. Note this does not verify the validity of the header packet, 162 // it just guarantees that the packet is marked as a header packet (i.e. 163 // it is definintely not a data packet). Do not use this to identify 164 // streams, use it to filter header packets from data packets while 165 // decoding. IsHeader(ogg_packet * aPacket)166 virtual bool IsHeader(ogg_packet* aPacket) { return false; } 167 168 // Returns true if the OggCodecState thinks this packet represents a 169 // keyframe, from which decoding can restart safely. IsKeyframe(ogg_packet * aPacket)170 virtual bool IsKeyframe(ogg_packet* aPacket) { return true; } 171 172 // Returns true if there is a packet available for dequeueing in the stream. 173 bool IsPacketReady(); 174 175 // Returns the next raw packet in the stream, or nullptr if there are no more 176 // packets buffered in the packet queue. More packets can be buffered by 177 // inserting one or more pages into the stream by calling PageIn(). The 178 // caller is responsible for deleting returned packet's using 179 // OggCodecState::ReleasePacket(). The packet will have a valid granulepos. 180 ogg_packet* PacketOut(); 181 182 // Returns the next raw packet in the stream, or nullptr if there are no more 183 // packets buffered in the packet queue, without consuming it. 184 // The packet will have a valid granulepos. 185 ogg_packet* PacketPeek(); 186 187 // Moves all raw packets from aOther to the front of the current packet queue. 188 void PushFront(OggPacketQueue&& aOther); 189 190 // Releases the memory used by a cloned packet. Every packet returned by 191 // PacketOut() must be free'd using this function. 192 static void ReleasePacket(ogg_packet* aPacket); 193 194 // Returns the next packet in the stream as a MediaRawData, or nullptr 195 // if there are no more packets buffered in the packet queue. More packets 196 // can be buffered by inserting one or more pages into the stream by calling 197 // PageIn(). The packet will have a valid granulepos. 198 virtual already_AddRefed<MediaRawData> PacketOutAsMediaRawData(); 199 200 // Extracts all packets from the page, and inserts them into the packet 201 // queue. They can be extracted by calling PacketOut(). Packets from an 202 // inactive stream are not buffered, i.e. this call has no effect for 203 // inactive streams. Multiple pages may need to be inserted before 204 // PacketOut() starts to return packets, as granulepos may need to be 205 // captured. 206 virtual nsresult PageIn(ogg_page* aPage); 207 208 // Number of packets read. 209 uint64_t mPacketCount; 210 211 // Serial number of the bitstream. 212 uint32_t mSerial; 213 214 // Ogg specific state. 215 ogg_stream_state mState; 216 217 // Queue of as yet undecoded packets. Packets are guaranteed to have 218 // a valid granulepos. 219 OggPacketQueue mPackets; 220 221 // Is the bitstream active; whether we're decoding and playing this bitstream. 222 bool mActive; 223 224 // True when all headers packets have been read. 225 bool mDoneReadingHeaders; 226 227 // Validation utility for vorbis-style tag names. 228 static bool IsValidVorbisTagName(nsCString& aName); 229 230 // Utility method to parse and add a vorbis-style comment 231 // to a metadata hash table. Most Ogg-encapsulated codecs 232 // use the vorbis comment format for metadata. 233 static bool AddVorbisComment(MetadataTags* aTags, 234 const char* aComment, 235 uint32_t aLength); 236 237 protected: 238 // Constructs a new OggCodecState. aActive denotes whether the stream is 239 // active. For streams of unsupported or unknown types, aActive should be 240 // false. 241 OggCodecState(ogg_page* aBosPage, bool aActive); 242 243 // Deallocates all packets stored in mUnstamped, and clears the array. 244 void ClearUnstamped(); 245 246 // Extracts packets out of mState until a data packet with a non -1 247 // granulepos is encountered, or no more packets are readable. Header 248 // packets are pushed into the packet queue immediately, and data packets 249 // are buffered in mUnstamped. Once a non -1 granulepos packet is read 250 // the granulepos of the packets in mUnstamped can be inferred, and they 251 // can be pushed over to mPackets. Used by PageIn() implementations in 252 // subclasses. 253 nsresult PacketOutUntilGranulepos(bool& aFoundGranulepos); 254 255 // Temporary buffer in which to store packets while we're reading packets 256 // in order to capture granulepos. 257 nsTArray<ogg_packet*> mUnstamped; 258 259 private: 260 bool InternalInit(); 261 }; 262 263 class VorbisState : public OggCodecState 264 { 265 public: 266 explicit VorbisState(ogg_page* aBosPage); 267 virtual ~VorbisState(); 268 GetType()269 CodecType GetType() override { return TYPE_VORBIS; } 270 bool DecodeHeader(ogg_packet* aPacket) override; 271 int64_t Time(int64_t granulepos) override; 272 int64_t PacketDuration(ogg_packet* aPacket) override; 273 bool Init() override; 274 nsresult Reset() override; 275 bool IsHeader(ogg_packet* aPacket) override; 276 nsresult PageIn(ogg_page* aPage) override; 277 278 // Return a hash table with tag metadata. 279 MetadataTags* GetTags() override; 280 281 // Returns the end time that a granulepos represents. 282 static int64_t Time(vorbis_info* aInfo, int64_t aGranulePos); 283 284 vorbis_info mInfo; 285 vorbis_comment mComment; 286 vorbis_dsp_state mDsp; 287 vorbis_block mBlock; 288 289 private: 290 291 // Reconstructs the granulepos of Vorbis packets stored in the mUnstamped 292 // array. 293 nsresult ReconstructVorbisGranulepos(); 294 295 // The "block size" of the previously decoded Vorbis packet, or 0 if we've 296 // not yet decoded anything. This is used to calculate the number of samples 297 // in a Vorbis packet, since each Vorbis packet depends on the previous 298 // packet while being decoded. 299 long mPrevVorbisBlockSize; 300 301 // Granulepos (end sample) of the last decoded Vorbis packet. This is used 302 // to calculate the Vorbis granulepos when we don't find a granulepos to 303 // back-propagate from. 304 int64_t mGranulepos; 305 306 #ifdef VALIDATE_VORBIS_SAMPLE_CALCULATION 307 // When validating that we've correctly predicted Vorbis packets' number 308 // of samples, we store each packet's predicted number of samples in this 309 // map, and verify we decode the predicted number of samples. 310 std::map<ogg_packet*, long> mVorbisPacketSamples; 311 #endif 312 313 // Records that aPacket is predicted to have aSamples samples. 314 // This function has no effect if VALIDATE_VORBIS_SAMPLE_CALCULATION 315 // is not defined. 316 void RecordVorbisPacketSamples(ogg_packet* aPacket, long aSamples); 317 318 // Verifies that aPacket has had its number of samples predicted. 319 // This function has no effect if VALIDATE_VORBIS_SAMPLE_CALCULATION 320 // is not defined. 321 void AssertHasRecordedPacketSamples(ogg_packet* aPacket); 322 323 public: 324 // Asserts that the number of samples predicted for aPacket is aSamples. 325 // This function has no effect if VALIDATE_VORBIS_SAMPLE_CALCULATION 326 // is not defined. 327 void ValidateVorbisPacketSamples(ogg_packet* aPacket, long aSamples); 328 329 }; 330 331 // Returns 1 if the Theora info struct is decoding a media of Theora 332 // version (maj,min,sub) or later, otherwise returns 0. 333 int TheoraVersion(th_info* info, 334 unsigned char maj, 335 unsigned char min, 336 unsigned char sub); 337 338 class TheoraState : public OggCodecState 339 { 340 public: 341 explicit TheoraState(ogg_page* aBosPage); 342 virtual ~TheoraState(); 343 GetType()344 CodecType GetType() override { return TYPE_THEORA; } 345 bool DecodeHeader(ogg_packet* aPacket) override; 346 int64_t Time(int64_t granulepos) override; 347 int64_t StartTime(int64_t granulepos) override; 348 int64_t PacketDuration(ogg_packet* aPacket) override; 349 bool Init() override; 350 bool IsHeader(ogg_packet* aPacket) override; 351 bool IsKeyframe(ogg_packet* aPacket) override; 352 nsresult PageIn(ogg_page* aPage) override; 353 354 // Returns the maximum number of microseconds which a keyframe can be offset 355 // from any given interframe. 356 int64_t MaxKeyframeOffset(); 357 358 // Returns the end time that a granulepos represents. 359 static int64_t Time(th_info* aInfo, int64_t aGranulePos); 360 361 th_info mInfo; 362 th_comment mComment; 363 th_setup_info* mSetup; 364 th_dec_ctx* mCtx; 365 366 float mPixelAspectRatio; 367 368 private: 369 370 // Reconstructs the granulepos of Theora packets stored in the 371 // mUnstamped array. mUnstamped must be filled with consecutive packets from 372 // the stream, with the last packet having a known granulepos. Using this 373 // known granulepos, and the known frame numbers, we recover the granulepos 374 // of all frames in the array. This enables us to determine their timestamps. 375 void ReconstructTheoraGranulepos(); 376 377 }; 378 379 class OpusState : public OggCodecState 380 { 381 public: 382 explicit OpusState(ogg_page* aBosPage); 383 virtual ~OpusState(); 384 GetType()385 CodecType GetType() override { return TYPE_OPUS; } 386 bool DecodeHeader(ogg_packet* aPacket) override; 387 int64_t Time(int64_t aGranulepos) override; 388 int64_t PacketDuration(ogg_packet* aPacket) override; 389 bool Init() override; 390 nsresult Reset() override; 391 nsresult Reset(bool aStart); 392 bool IsHeader(ogg_packet* aPacket) override; 393 nsresult PageIn(ogg_page* aPage) override; 394 already_AddRefed<MediaRawData> PacketOutAsMediaRawData() override; 395 // Returns the end time that a granulepos represents. 396 static int64_t Time(int aPreSkip, int64_t aGranulepos); 397 398 // Various fields from the Ogg Opus header. 399 int mRate; // Sample rate the decoder uses (always 48 kHz). 400 int mChannels; // Number of channels the stream encodes. 401 uint16_t mPreSkip; // Number of samples to strip after decoder reset. 402 #ifdef MOZ_SAMPLE_TYPE_FLOAT32 403 float mGain; // Gain to apply to decoder output. 404 #else 405 int32_t mGain_Q16; // Gain to apply to the decoder output. 406 #endif 407 408 nsAutoPtr<OpusParser> mParser; 409 OpusMSDecoder* mDecoder; 410 411 int mSkip; // Number of samples left to trim before playback. 412 // Granule position (end sample) of the last decoded Opus packet. This is 413 // used to calculate the amount we should trim from the last packet. 414 int64_t mPrevPacketGranulepos; 415 416 // Construct and return a table of tags from the metadata header. 417 MetadataTags* GetTags() override; 418 419 private: 420 421 // Reconstructs the granulepos of Opus packets stored in the 422 // mUnstamped array. mUnstamped must be filled with consecutive packets from 423 // the stream, with the last packet having a known granulepos. Using this 424 // known granulepos, and the known frame numbers, we recover the granulepos 425 // of all frames in the array. This enables us to determine their timestamps. 426 bool ReconstructOpusGranulepos(); 427 428 // Granule position (end sample) of the last decoded Opus page. This is 429 // used to calculate the Opus per-packet granule positions on the last page, 430 // where we may need to trim some samples from the end. 431 int64_t mPrevPageGranulepos; 432 433 }; 434 435 // Constructs a 32bit version number out of two 16 bit major,minor 436 // version numbers. 437 #define SKELETON_VERSION(major, minor) (((major)<<16)|(minor)) 438 439 enum EMsgHeaderType { 440 eContentType, 441 eRole, 442 eName, 443 eLanguage, 444 eTitle, 445 eDisplayHint, 446 eAltitude, 447 eTrackOrder, 448 eTrackDependencies 449 }; 450 451 typedef struct 452 { 453 const char* mPatternToRecognize; 454 EMsgHeaderType mMsgHeaderType; 455 } FieldPatternType; 456 457 // Stores the message information for different logical bitstream. 458 typedef struct 459 { 460 nsClassHashtable<nsUint32HashKey, nsCString> mValuesStore; 461 } MessageField; 462 463 class SkeletonState : public OggCodecState 464 { 465 public: 466 explicit SkeletonState(ogg_page* aBosPage); 467 ~SkeletonState(); 468 469 nsClassHashtable<nsUint32HashKey, MessageField> mMsgFieldStore; 470 GetType()471 CodecType GetType() override { return TYPE_SKELETON; } 472 bool DecodeHeader(ogg_packet* aPacket) override; Time(int64_t granulepos)473 int64_t Time(int64_t granulepos) override { return -1; } IsHeader(ogg_packet * aPacket)474 bool IsHeader(ogg_packet* aPacket) override { return true; } 475 476 // Return true if the given time (in milliseconds) is within 477 // the presentation time defined in the skeleton track. IsPresentable(int64_t aTime)478 bool IsPresentable(int64_t aTime) { return aTime >= mPresentationTime; } 479 480 // Stores the offset of the page on which a keyframe starts, 481 // and its presentation time. 482 class nsKeyPoint 483 { 484 public: nsKeyPoint()485 nsKeyPoint() 486 : mOffset(INT64_MAX) 487 , mTime(INT64_MAX) {} 488 nsKeyPoint(int64_t aOffset,int64_t aTime)489 nsKeyPoint(int64_t aOffset, int64_t aTime) 490 : mOffset(aOffset) 491 ,mTime(aTime) {} 492 493 // Offset from start of segment/link-in-the-chain in bytes. 494 int64_t mOffset; 495 496 // Presentation time in usecs. 497 int64_t mTime; 498 IsNull()499 bool IsNull() 500 { 501 return mOffset == INT64_MAX && mTime == INT64_MAX; 502 } 503 }; 504 505 // Stores a keyframe's byte-offset, presentation time and the serialno 506 // of the stream it belongs to. 507 class nsSeekTarget 508 { 509 public: nsSeekTarget()510 nsSeekTarget() : mSerial(0) {} 511 nsKeyPoint mKeyPoint; 512 uint32_t mSerial; IsNull()513 bool IsNull() 514 { 515 return mKeyPoint.IsNull() && mSerial == 0; 516 } 517 }; 518 519 // Determines from the seek index the keyframe which you must seek back to 520 // in order to get all keyframes required to render all streams with 521 // serialnos in aTracks, at time aTarget. 522 nsresult IndexedSeekTarget(int64_t aTarget, 523 nsTArray<uint32_t>& aTracks, 524 nsSeekTarget& aResult); 525 HasIndex()526 bool HasIndex() const 527 { 528 return mIndex.Count() > 0; 529 } 530 531 // Returns the duration of the active tracks in the media, if we have 532 // an index. aTracks must be filled with the serialnos of the active tracks. 533 // The duration is calculated as the greatest end time of all active tracks, 534 // minus the smalled start time of all the active tracks. 535 nsresult GetDuration(const nsTArray<uint32_t>& aTracks, int64_t& aDuration); 536 537 private: 538 539 // Decodes an index packet. Returns false on failure. 540 bool DecodeIndex(ogg_packet* aPacket); 541 // Decodes an fisbone packet. Returns false on failure. 542 bool DecodeFisbone(ogg_packet* aPacket); 543 544 // Gets the keypoint you must seek to in order to get the keyframe required 545 // to render the stream at time aTarget on stream with serial aSerialno. 546 nsresult IndexedSeekTargetForTrack(uint32_t aSerialno, 547 int64_t aTarget, 548 nsKeyPoint& aResult); 549 550 // Version of the decoded skeleton track, as per the SKELETON_VERSION macro. 551 uint32_t mVersion; 552 553 // Presentation time of the resource in milliseconds 554 int64_t mPresentationTime; 555 556 // Length of the resource in bytes. 557 int64_t mLength; 558 559 // Stores the keyframe index and duration information for a particular 560 // stream. 561 class nsKeyFrameIndex 562 { 563 public: 564 nsKeyFrameIndex(int64_t aStartTime,int64_t aEndTime)565 nsKeyFrameIndex(int64_t aStartTime, int64_t aEndTime) 566 : mStartTime(aStartTime) 567 , mEndTime(aEndTime) 568 { 569 MOZ_COUNT_CTOR(nsKeyFrameIndex); 570 } 571 ~nsKeyFrameIndex()572 ~nsKeyFrameIndex() 573 { 574 MOZ_COUNT_DTOR(nsKeyFrameIndex); 575 } 576 Add(int64_t aOffset,int64_t aTimeMs)577 void Add(int64_t aOffset, int64_t aTimeMs) 578 { 579 mKeyPoints.AppendElement(nsKeyPoint(aOffset, aTimeMs)); 580 } 581 Get(uint32_t aIndex)582 const nsKeyPoint& Get(uint32_t aIndex) const 583 { 584 return mKeyPoints[aIndex]; 585 } 586 Length()587 uint32_t Length() const 588 { 589 return mKeyPoints.Length(); 590 } 591 592 // Presentation time of the first sample in this stream in usecs. 593 const int64_t mStartTime; 594 595 // End time of the last sample in this stream in usecs. 596 const int64_t mEndTime; 597 598 private: 599 nsTArray<nsKeyPoint> mKeyPoints; 600 }; 601 602 // Maps Ogg serialnos to the index-keypoint list. 603 nsClassHashtable<nsUint32HashKey, nsKeyFrameIndex> mIndex; 604 }; 605 606 class FlacState : public OggCodecState 607 { 608 public: 609 explicit FlacState(ogg_page* aBosPage); 610 GetType()611 CodecType GetType() override { return TYPE_FLAC; } 612 bool DecodeHeader(ogg_packet* aPacket) override; 613 int64_t Time(int64_t granulepos) override; 614 int64_t PacketDuration(ogg_packet* aPacket) override; 615 bool IsHeader(ogg_packet* aPacket) override; 616 nsresult PageIn(ogg_page* aPage) override; 617 618 // Return a hash table with tag metadata. 619 MetadataTags* GetTags() override; 620 621 const AudioInfo& Info(); 622 623 private: 624 bool ReconstructFlacGranulepos(void); 625 626 FlacFrameParser mParser; 627 }; 628 629 } // namespace mozilla 630 631 // This allows the use of nsAutoRefs for an ogg_packet that properly free the 632 // contents of the packet. 633 template <> 634 class nsAutoRefTraits<ogg_packet> : public nsPointerRefTraits<ogg_packet> 635 { 636 public: Release(ogg_packet * aPacket)637 static void Release(ogg_packet* aPacket) 638 { 639 mozilla::OggCodecState::ReleasePacket(aPacket); 640 } 641 }; 642 643 644 #endif 645