private void loopInternal() throws IOException, InterruptedException {
            // If this is an online stream, notify the client of the buffer fill level.
            long cachedDuration = mDecoders.getCachedDuration();
            if(cachedDuration != -1) {
                // The cached duration from the MediaExtractor returns the cached time from
                // the current position onwards, but the Android MediaPlayer returns the
                // total time consisting of the current playback point and the length of
                // the prefetched data.
                // This comes before the buffering pause to update the clients buffering info
                // also during a buffering playback pause.
                // Use the PTS of the decoder input for a more precise buffer calculation (the
                // cached duration is the duration from the read position in the media extractor,
                // which is about the same position as the position from where the last sample
                // has been read).
                long currentPosition = mDecoders.getInputSamplePTS();
                if (currentPosition == MediaCodecDecoder.PTS_UNKNOWN) {
                    // If this input PTS is not available, e.g. directly after a seek, fall
                    // back to the current playback position.
                    currentPosition = mCurrentPosition;
                }
                updateBufferPercentage((int) (100d / (getDuration() * 1000) * (currentPosition + cachedDuration)));
            }

            // If we are in buffering mode, check if the buffer has been filled until the low water
            // mark or the end of the stream has been reached, and pause playback if it isn't filled
            // high enough yet.
            if(mBuffering && cachedDuration > -1 && cachedDuration < BUFFER_LOW_WATER_MARK_US && !mDecoders.hasCacheReachedEndOfStream()) {
                //Log.d(TAG, "buffering... " + mDecoders.getCachedDuration() + " / " + BUFFER_LOW_WATER_MARK_US);
                // To pause playback for buffering, we simply skip this loop and call it again later
                mHandler.sendEmptyMessageDelayed(PLAYBACK_LOOP, 100);
                return;
            }

            if(mDecoders.getVideoDecoder() != null && mVideoFrameInfo == null) {
                // This method needs a video frame to operate on. If there is no frame, we need
                // to decode one first.
                mVideoFrameInfo = mDecoders.decodeFrame(false);
                if(mVideoFrameInfo == null && !mDecoders.isEOS()) {
                    // If the decoder didn't return a frame, we need to give it some processing time
                    // and come back later...
                    mHandler.sendEmptyMessageDelayed(PLAYBACK_LOOP, 10);
                    return;
                }
            }

            long startTime = SystemClock.elapsedRealtime();

            // When we are in buffering mode, and a frame has been decoded, the buffer is
            // obviously refilled so we can send the buffering end message and exit buffering mode.
            if(mBuffering) {
                mBuffering = false;
                mEventHandler.sendMessage(mEventHandler.obtainMessage(MEDIA_INFO,
                        MEDIA_INFO_BUFFERING_END, 0));

                // Reset timebase so player does not try to catch up time lost while caching
                mTimeBase.startAt(mDecoders.getCurrentDecodingPTS());
            }

            // When the waiting time to the next frame is too long, we defer rendering through
            // the handler here instead of relying on releaseOutputBuffer(buffer, renderTimestampNs),
            // which does not work well with long waiting times and many frames in the queue.
            // On API < 21 the frame rendering is timed with a sleep() and this is not really necessary,
            // but still shifts some waiting time from the sleep() to here.
            if(mVideoFrameInfo != null && mTimeBase.getOffsetFrom(mVideoFrameInfo.presentationTimeUs) > 60000) {
                mHandler.sendEmptyMessageDelayed(PLAYBACK_LOOP, 50);
                return;
            }

            // Update the current position of the player
            mCurrentPosition = mDecoders.getCurrentDecodingPTS();

            // fire cue events
            // Rate limited to 10 Hz (every 100ms)
            if (mCueTimeline.count() > 0 && startTime - mLastCueEventTime > 100) {
                mLastCueEventTime = startTime;
                mCueTimeline.movePlaybackPosition((int) (mCurrentPosition / 1000),
                        mOnTimelineCueListener);
            }

            if(mDecoders.getVideoDecoder() != null && mVideoFrameInfo != null) {
                renderVideoFrame(mVideoFrameInfo);
                mVideoFrameInfo = null;

                // When the first frame is rendered, video rendering has started and the event triggered
                if (mRenderingStarted) {
                    mRenderingStarted = false;
                    mEventHandler.sendMessage(mEventHandler.obtainMessage(MEDIA_INFO,
                            MEDIA_INFO_VIDEO_RENDERING_START, 0));
                }
            }

            if (mAudioPlayback != null) {
                // Sync audio playback speed to playback speed (to account for speed changes during playback)
                // Change the speed on the audio playback object only if it has really changed, to avoid runtime overhead
                if(mPlaybackSpeed != mTimeBase.getSpeed()) {
                    mPlaybackSpeed = mTimeBase.getSpeed();
                    mAudioPlayback.setPlaybackSpeed((float) mPlaybackSpeed);
                }

                // Sync timebase to audio timebase when there is audio data available
                long currentAudioPTS = mAudioPlayback.getCurrentPresentationTimeUs();
                if(currentAudioPTS > AudioPlayback.PTS_NOT_SET) {
                    mTimeBase.startAt(currentAudioPTS);
                }
            }

            // Handle EOS
            if (mDecoders.isEOS()) {
                mEventHandler.sendEmptyMessage(MEDIA_PLAYBACK_COMPLETE);

                // If looping is on, seek back to the start...
                if(mLooping) {
                    if(mAudioPlayback != null) {
                        // Flush audio buffer to reset audio PTS
                        mAudioPlayback.flush();
                    }
                    mDecoders.seekTo(SeekMode.FAST_TO_PREVIOUS_SYNC, 0);
                    mCueTimeline.setPlaybackPosition(0);
                    mDecoders.renderFrames();
                }
                // ... else just pause playback and wait for next command
                else {
                    mPaused = true;
                    pauseInternal(true); // pause but play remaining buffered audio
                }
            } else {
                // Get next frame
                mVideoFrameInfo = mDecoders.decodeFrame(false);
            }

            if(!mPaused) {
                // Static delay time until the next call of the playback loop
                long delay = 10;
                // Scale delay by playback speed to avoid limiting framerate
                delay = (long)(delay / mTimeBase.getSpeed());
                // Calculate the duration taken for the current call
                long duration = (SystemClock.elapsedRealtime() - startTime);
                // Adjust the delay by the time taken
                delay = delay - duration;
                if(delay > 0) {
                    // Sleep for some time and then continue processing the loop
                    // This replaces the very unreliable and jittery Thread.sleep in the old decoder thread
                    mHandler.sendEmptyMessageDelayed(PLAYBACK_LOOP, delay);
                } else {
                    // The current call took too much time; there is no time left for delaying, call instantly
                    mHandler.sendEmptyMessage(PLAYBACK_LOOP);
                }
            }
        }
Removes a cue added by {@link #addCue(int, Object)} from the playback timeline.

@param cue the cue to remove
@return true if removal was successful, false if the cue wasn't cued on the timeline,
i.e. it has already been removed before
MediaPlayer/src/main/java/net/protyposis/android/mediaplayer/MediaPlayer.java