package tv.emby.embyatv.playback; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Handler; import android.preference.PreferenceManager; import android.view.View; import java.util.List; import mediabrowser.apiinteraction.ApiClient; import mediabrowser.apiinteraction.Response; import mediabrowser.apiinteraction.android.profiles.AndroidProfile; import mediabrowser.model.dlna.DeviceProfile; import mediabrowser.model.dlna.DirectPlayProfile; import mediabrowser.model.dlna.PlaybackException; import mediabrowser.model.dlna.StreamInfo; import mediabrowser.model.dlna.SubtitleDeliveryMethod; import mediabrowser.model.dlna.SubtitleStreamInfo; import mediabrowser.model.dlna.VideoOptions; import mediabrowser.model.dto.BaseItemDto; import mediabrowser.model.dto.MediaSourceInfo; import mediabrowser.model.dto.UserItemDataDto; import mediabrowser.model.entities.LocationType; import mediabrowser.model.entities.MediaStream; import mediabrowser.model.entities.MediaStreamType; import mediabrowser.model.extensions.StringHelper; import mediabrowser.model.library.PlayAccess; import mediabrowser.model.livetv.ChannelInfoDto; import mediabrowser.model.mediainfo.SubtitleTrackInfo; import mediabrowser.model.session.PlayMethod; import tv.emby.embyatv.R; import tv.emby.embyatv.TvApp; import tv.emby.embyatv.livetv.TvManager; import tv.emby.embyatv.ui.ImageButton; import tv.emby.embyatv.util.ProfileHelper; import tv.emby.embyatv.util.Utils; /** * Created by Eric on 12/9/2014. */ public class PlaybackController { List<BaseItemDto> mItems; VideoManager mVideoManager; SubtitleHelper mSubHelper; int mCurrentIndex = 0; private long mCurrentPosition = 0; private PlaybackState mPlaybackState = PlaybackState.IDLE; private TvApp mApplication; private StreamInfo mCurrentStreamInfo; private List<SubtitleStreamInfo> mSubtitleStreams; private IPlaybackOverlayFragment mFragment; private View mSpinner; private Boolean spinnerOff = false; private VideoOptions mCurrentOptions; private int mDefaultSubIndex = -1; private int mDefaultAudioIndex = -1; private PlayMethod mPlaybackMethod = PlayMethod.Transcode; private Runnable mReportLoop; private Handler mHandler; private static int REPORT_INTERVAL = 3000; private long mNextItemThreshold = Long.MAX_VALUE; private boolean nextItemReported; private long mStartPosition = 0; private long mCurrentProgramEndTime; private long mCurrentProgramStartTime; private boolean isLiveTv; private String liveTvChannelName = ""; private boolean useVlc = false; private boolean updateProgress = true; public PlaybackController(List<BaseItemDto> items, IPlaybackOverlayFragment fragment) { mItems = items; mFragment = fragment; mApplication = TvApp.getApplication(); mHandler = new Handler(); mSubHelper = new SubtitleHelper(TvApp.getApplication().getCurrentActivity()); } public void init(VideoManager mgr, View spinner) { mVideoManager = mgr; mSpinner = spinner; setupCallbacks(); } public PlayMethod getPlaybackMethod() { return mPlaybackMethod; } public void setPlaybackMethod(PlayMethod value) { mPlaybackMethod = value; } public BaseItemDto getCurrentlyPlayingItem() { return mItems.get(mCurrentIndex); } public MediaSourceInfo getCurrentMediaSource() { return mCurrentStreamInfo != null && mCurrentStreamInfo.getMediaSource() != null ? mCurrentStreamInfo.getMediaSource() : getCurrentlyPlayingItem().getMediaSources().get(0);} public StreamInfo getCurrentStreamInfo() { return mCurrentStreamInfo; } public boolean canSeek() {return !isLiveTv;} public boolean isLiveTv() { return isLiveTv; } public int getSubtitleStreamIndex() {return (mCurrentOptions != null && mCurrentOptions.getSubtitleStreamIndex() != null) ? mCurrentOptions.getSubtitleStreamIndex() : -1; } public Integer getAudioStreamIndex() { return isTranscoding() ? mCurrentStreamInfo.getAudioStreamIndex() != null ? mCurrentStreamInfo.getAudioStreamIndex() : mCurrentOptions.getAudioStreamIndex() : mVideoManager.getAudioTrack() > -1 ? Integer.valueOf(mVideoManager.getAudioTrack()) : bestGuessAudioTrack(getCurrentMediaSource()); } public List<SubtitleStreamInfo> getSubtitleStreams() { return mSubtitleStreams; } public SubtitleStreamInfo getSubtitleStreamInfo(int index) { for (SubtitleStreamInfo info : mSubtitleStreams) { if (info.getIndex() == index) return info; } return null; } public boolean isNativeMode() { return mVideoManager == null || mVideoManager.isNativeMode(); } public boolean isTranscoding() { return mCurrentStreamInfo != null && mCurrentStreamInfo.getPlayMethod() == PlayMethod.Transcode; } public boolean hasNextItem() { return mCurrentIndex < mItems.size() - 1; } public BaseItemDto getNextItem() { return hasNextItem() ? mItems.get(mCurrentIndex+1) : null; } public boolean isPlaying() { return mPlaybackState == PlaybackState.PLAYING; } public void setAudioDelay(long value) { if (mVideoManager != null) mVideoManager.setAudioDelay(value);} public long getAudioDelay() { return mVideoManager != null ? mVideoManager.getAudioDelay() : 0;} private Integer bestGuessAudioTrack(MediaSourceInfo info) { if (info != null) { boolean videoFound = false; for (MediaStream track : info.getMediaStreams()) { if (track.getType() == MediaStreamType.Video) { videoFound = true; } else { if (videoFound && track.getType() == MediaStreamType.Audio) return track.getIndex(); } } } return null; } public void play(long position) { play(position, -1); } private void play(long position, int transcodedSubtitle) { if (!TvApp.getApplication().isValid()) { Utils.showToast(TvApp.getApplication(), "Playback not supported. Please unlock or become a supporter."); return; } if (TvApp.getApplication().isTrial()) { Utils.showToast(TvApp.getApplication(), TvApp.getApplication().getRegistrationString()+". Unlock or become a supporter for unlimited playback."); } mApplication.getLogger().Debug("Play called with pos: " + position + " and sub index: "+transcodedSubtitle); switch (mPlaybackState) { case PLAYING: // do nothing break; case PAUSED: // just resume mVideoManager.play(); if (mVideoManager.isNativeMode()) mPlaybackState = PlaybackState.PLAYING; //won't get another onprepared call if (mFragment != null) { mFragment.setFadingEnabled(true); mFragment.setPlayPauseActionState(ImageButton.STATE_SECONDARY); mFragment.updateEndTime(mVideoManager.getDuration() - getCurrentPosition()); } startReportLoop(); break; case BUFFERING: // onPrepared should take care of it break; case IDLE: // start new playback BaseItemDto item = getCurrentlyPlayingItem(); lastProgressPosition = 0; // make sure item isn't missing if (item.getLocationType() == LocationType.Virtual) { if (hasNextItem()) { new AlertDialog.Builder(mApplication.getCurrentActivity()) .setTitle("Episode Missing") .setMessage("This episode is missing from your library. Would you like to skip it and continue to the next one?") .setPositiveButton(mApplication.getResources().getString(R.string.lbl_yes), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { next(); } }) .setNegativeButton(mApplication.getResources().getString(R.string.lbl_no), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mApplication.getCurrentActivity().finish(); } }) .create() .show(); return; } else { new AlertDialog.Builder(mApplication.getCurrentActivity()) .setTitle("Episode Missing") .setMessage("This episode is missing from your library. Playback will stop.") .setPositiveButton(mApplication.getResources().getString(R.string.lbl_ok), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mApplication.getCurrentActivity().finish(); } }) .create() .show(); return; } } // confirm we actually can play if (item.getPlayAccess() != PlayAccess.Full) { String msg = item.getIsPlaceHolder() ? mApplication.getString(R.string.msg_cannot_play) : mApplication.getString(R.string.msg_cannot_play_time); Utils.showToast(TvApp.getApplication(), msg); return; } startSpinner(); mCurrentOptions = new VideoOptions(); mCurrentOptions.setDeviceId(mApplication.getApiClient().getDeviceId()); mCurrentOptions.setItemId(item.getId()); mCurrentOptions.setMediaSources(item.getMediaSources()); mCurrentOptions.setMaxBitrate(getMaxBitrate()); if (Utils.downMixAudio()) mCurrentOptions.setMaxAudioChannels(2); if (!mVideoManager.isNativeMode()) { mCurrentOptions.setSubtitleStreamIndex(transcodedSubtitle >= 0 ? transcodedSubtitle : null); mCurrentOptions.setMediaSourceId(transcodedSubtitle >= 0 ? getCurrentMediaSource().getId() : null); } else { TvApp.getApplication().getLogger().Info("Transcoded subtitle requested. Will switch to VLC to embed"); } mDefaultSubIndex = transcodedSubtitle; TvApp.getApplication().getLogger().Debug("Max bitrate is: " + getMaxBitrate()); isLiveTv = item.getType().equals("TvChannel"); // Create our profile - use VLC unless live tv or on FTV stick and over SD useVlc = (transcodedSubtitle >= 0 || (Utils.downMixAudio() && !isLiveTv) || ((!Utils.is60() && (!isLiveTv || mApplication.directStreamLiveTv())) || (isLiveTv && mApplication.directStreamLiveTv()) || (item.getPath() != null && item.getPath().toLowerCase().endsWith(".ts"))) && (!"ChannelVideoItem".equals(item.getType())) && TvApp.getApplication().getPrefs().getBoolean("pref_enable_vlc", true) && (item.getPath() == null || !item.getPath().toLowerCase().endsWith(".avi"))); if (useVlc && item.getMediaSources() != null && item.getMediaSources().size() > 0) { List<MediaStream> videoStreams = Utils.GetVideoStreams(item.getMediaSources().get(0)); MediaStream video = videoStreams != null && videoStreams.size() > 0 ? videoStreams.get(0) : null; if (video != null && video.getWidth() > (Utils.isFireTvStick() ? 730 : Integer.parseInt(mApplication.getPrefs().getString("pref_vlc_max_res", "730")))) { useVlc = false; mApplication.getLogger().Info("Forcing a transcode of HD content"); } } else { useVlc = useVlc && !Utils.isFireTvStick(); } DeviceProfile profile = ProfileHelper.getBaseProfile(); if (useVlc) { ProfileHelper.setVlcOptions(profile); TvApp.getApplication().getLogger().Info("*** Using VLC profile options"); } else { if (Utils.is60()) { ProfileHelper.setExoOptions(profile, isLiveTv, true); ProfileHelper.addAc3Streaming(profile, true); TvApp.getApplication().getLogger().Info("*** Using extended Exoplayer profile options for 6.0+"); } else { TvApp.getApplication().getLogger().Info("*** Using default android profile"); } } mCurrentOptions.setProfile(profile); playInternal(getCurrentlyPlayingItem(), position, mCurrentOptions); mPlaybackState = PlaybackState.BUFFERING; if (mFragment != null) { mFragment.setPlayPauseActionState(ImageButton.STATE_SECONDARY); mFragment.setFadingEnabled(true); mFragment.setCurrentTime(position); } long duration = getCurrentlyPlayingItem().getRunTimeTicks()!= null ? getCurrentlyPlayingItem().getRunTimeTicks() / 10000 : -1; mVideoManager.setMetaDuration(duration); if (hasNextItem()) { // Determine the "next up" threshold if (duration > 600000) { //only items longer than 10min to have this feature nextItemReported = false; if (duration > 4500000) { //longer than 1hr 15 it probably has pretty long credits mNextItemThreshold = duration - 180000; // 3 min } else { //std 30 min episode or less mNextItemThreshold = duration - 50000; // 50 seconds } TvApp.getApplication().getLogger().Debug("Next item threshold set to "+ mNextItemThreshold); } else { mNextItemThreshold = Long.MAX_VALUE; } } else { mNextItemThreshold = Long.MAX_VALUE; } break; } } public int getMaxBitrate() { SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(mApplication); String maxRate = sharedPref.getString("pref_max_bitrate", "0"); Float factor = Float.parseFloat(maxRate) * 10; return factor == 0 ? TvApp.getApplication().getAutoBitrate() : (factor.intValue() * 100000); } public int getBufferAmount() { return 600; } private void playInternal(final BaseItemDto item, final long position, final VideoOptions options) { final ApiClient apiClient = mApplication.getApiClient(); mApplication.setCurrentPlayingItem(item); if (isLiveTv) { liveTvChannelName = " ("+item.getName()+")"; updateTvProgramInfo(); TvManager.setLastLiveTvChannel(item.getId()); } mApplication.getPlaybackManager().getVideoStreamInfo(apiClient.getServerInfo().getId(), options, false, apiClient, new Response<StreamInfo>() { @Override public void onResponse(StreamInfo response) { if (!useVlc && (options.getAudioStreamIndex() != null && !options.getAudioStreamIndex().equals(bestGuessAudioTrack(response.getMediaSource())))) { // requested specific audio stream that is different from default so we need to force a transcode to get it (ExoMedia currently cannot switch) // remove direct play profiles to force the transcode final DeviceProfile save = options.getProfile(); DeviceProfile newProfile = ProfileHelper.getBaseProfile(); ProfileHelper.setExoOptions(newProfile, isLiveTv, true); ProfileHelper.addAc3Streaming(newProfile, true); newProfile.setDirectPlayProfiles(new DirectPlayProfile[]{}); options.setProfile(newProfile); mApplication.getLogger().Info("Forcing transcode due to non-default audio chosen"); mApplication.getPlaybackManager().getVideoStreamInfo(apiClient.getServerInfo().getId(), options, false, apiClient, new Response<StreamInfo>() { @Override public void onResponse(StreamInfo response) { //re-set this options.setProfile(save); startItem(item, position, apiClient, response); } }); } else if (useVlc && !Utils.is60() && mDefaultSubIndex < 0 && !isLiveTv && !Utils.downMixAudio() && TvApp.getApplication().getPrefs().getBoolean("pref_bitstream_ac3", true)) { MediaStream audio = response.getMediaSource().getDefaultAudioStream(); if (audio != null && ("ac3".equals(audio.getCodec()) || "eac3".equals(audio.getCodec()))) { // Use Exo to get DD bitstreaming final DeviceProfile save = options.getProfile(); DeviceProfile newProfile = ProfileHelper.getBaseProfile(); ProfileHelper.setExoOptions(newProfile, false, true); ProfileHelper.addAc3Streaming(newProfile, true); options.setProfile(newProfile); useVlc = false; mApplication.getLogger().Info("Using Exo for DD bitstreaming"); mApplication.getPlaybackManager().getVideoStreamInfo(apiClient.getServerInfo().getId(), options, false, apiClient, new Response<StreamInfo>() { @Override public void onResponse(StreamInfo response) { //re-set this options.setProfile(save); startItem(item, position, apiClient, response); } }); } else { startItem(item, position, apiClient, response); } } else { startItem(item, position, apiClient, response); } } @Override public void onError(Exception exception) { mApplication.getLogger().ErrorException("Error getting playback stream info", exception); if (exception instanceof PlaybackException) { PlaybackException ex = (PlaybackException) exception; switch (ex.getErrorCode()) { case NotAllowed: Utils.showToast(TvApp.getApplication(), TvApp.getApplication().getString(R.string.msg_playback_not_allowed)); break; case NoCompatibleStream: Utils.showToast(TvApp.getApplication(), TvApp.getApplication().getString(R.string.msg_playback_incompatible)); break; case RateLimitExceeded: Utils.showToast(TvApp.getApplication(), TvApp.getApplication().getString(R.string.msg_playback_restricted)); break; } } } }); } private void startItem(BaseItemDto item, long position, ApiClient apiClient, StreamInfo response) { mCurrentStreamInfo = response; Long mbPos = position * 10000; setPlaybackMethod(response.getPlayMethod()); if (useVlc && !getPlaybackMethod().equals(PlayMethod.Transcode)) { mVideoManager.setNativeMode(false); } else { mVideoManager.setNativeMode(true); TvApp.getApplication().getLogger().Info("Playing back in native mode."); if (Utils.downMixAudio()) { TvApp.getApplication().getLogger().Info("Setting max audio to 2-channels"); mCurrentStreamInfo.setMaxAudioChannels(2); } } // get subtitle info mSubtitleStreams = response.GetSubtitleProfiles(false, mApplication.getApiClient().getApiUrl(), mApplication.getApiClient().getAccessToken()); // set start point if transcoding to mkv if (mPlaybackMethod == PlayMethod.Transcode && response.getContainer().equals("mkv")) { response.setStartPositionTicks(position * 10000); } mFragment.updateDisplay(); String path = response.ToUrl(apiClient.getApiUrl(), apiClient.getAccessToken()); // if source is stereo or we're not on at least 5.1.1 with AC3 - use most compatible output if (!mVideoManager.isNativeMode() && (isLiveTv && !Utils.isGreaterThan51()) || (response.getMediaSource() != null && response.getMediaSource().getDefaultAudioStream() != null && response.getMediaSource().getDefaultAudioStream().getChannels() != null && (response.getMediaSource().getDefaultAudioStream().getChannels() <= 2 || (!Utils.isGreaterThan51() && "ac3".equals(response.getMediaSource().getDefaultAudioStream().getCodec()))))) { mVideoManager.setCompatibleAudio(); mApplication.getLogger().Info("Setting compatible audio mode..."); //Utils.showToast(mApplication, "Compatible"); } else { //Utils.showToast(mApplication, "Default"); mVideoManager.setAudioMode(); } mVideoManager.setVideoPath(path); mVideoManager.setVideoTrack(response.getMediaSource()); //wait a beat before attempting to start so the player surface is fully initialized and video is ready mHandler.postDelayed(new Runnable() { @Override public void run() { mVideoManager.start(); } },750); mStartPosition = position; mDefaultAudioIndex = getDefaultAudioIndex(response); mDefaultSubIndex = mPlaybackMethod != PlayMethod.Transcode && response.getMediaSource().getDefaultSubtitleStreamIndex() != null ? response.getMediaSource().getDefaultSubtitleStreamIndex() : mDefaultSubIndex; mApplication.setLastPlayedItem(item); if (!isRestart) Utils.ReportStart(item, mbPos); isRestart = false; } private int getDefaultAudioIndex(StreamInfo info) { return mPlaybackMethod != PlayMethod.Transcode && info.getMediaSource().getDefaultAudioStreamIndex() != null ? info.getMediaSource().getDefaultAudioStreamIndex() : -1; } public void startSpinner() { if (mApplication.getCurrentActivity() != null) { mApplication.getCurrentActivity().runOnUiThread(new Runnable() { @Override public void run() { if (mSpinner != null) mSpinner.setVisibility(View.VISIBLE); spinnerOff = false; } }); } } public void stopSpinner() { if (mApplication.getCurrentActivity() != null) { mApplication.getCurrentActivity().runOnUiThread(new Runnable() { @Override public void run() { spinnerOff = true; if (mSpinner != null) mSpinner.setVisibility(View.GONE); } }); } } public void switchAudioStream(int index) { if (!isPlaying()) return; mCurrentOptions.setAudioStreamIndex(index); if (mVideoManager.isNativeMode()) { startSpinner(); mApplication.getLogger().Debug("Setting audio index to: " + index); mCurrentOptions.setMediaSourceId(getCurrentMediaSource().getId()); stop(); playInternal(getCurrentlyPlayingItem(), mCurrentPosition, mCurrentOptions); mPlaybackState = PlaybackState.BUFFERING; } else { mVideoManager.setAudioTrack(index); if (!Utils.supportsAc3() && "ac3".equals(getCurrentMediaSource().getMediaStreams().get(index).getCodec())) { mVideoManager.setCompatibleAudio(); } else { mVideoManager.setAudioMode(); } } } private boolean burningSubs = false; public void switchSubtitleStream(int index) { mApplication.getLogger().Debug("Setting subtitle index to: " + index); mCurrentOptions.setSubtitleStreamIndex(index >= 0 ? index : null); if (index < 0) { if (burningSubs) { stop(); play(mCurrentPosition); burningSubs = false; } else { mFragment.addManualSubtitles(null); mVideoManager.disableSubs(); } return; } MediaStream stream = Utils.GetMediaStream(getCurrentMediaSource(), index); if (stream == null) { Utils.showToast(mApplication, "Unable to select subtitle"); return; } // handle according to delivery method SubtitleStreamInfo streamInfo = getSubtitleStreamInfo(index); if (streamInfo == null) { Utils.showToast(mApplication, mApplication.getResources().getString(R.string.msg_unable_load_subs)); } else { switch (streamInfo.getDeliveryMethod()) { case Encode: // Gonna need to burn in so start a transcode with the sub index stop(); if (!mVideoManager.isNativeMode()) { Utils.showToast(mApplication, mApplication.getResources().getString(R.string.msg_burn_sub_warning)); } play(mCurrentPosition, index); break; case Embed: if (!mVideoManager.isNativeMode()) { mFragment.addManualSubtitles(null); // in case these were on if (!mVideoManager.setSubtitleTrack(index, getCurrentlyPlayingItem().getMediaStreams())) { // error selecting internal subs Utils.showToast(mApplication, mApplication.getResources().getString(R.string.msg_unable_load_subs)); } break; } // not using vlc - fall through to external handling case External: mFragment.addManualSubtitles(null); mVideoManager.disableSubs(); mFragment.showSubLoadingMsg(true); stream.setDeliveryMethod(SubtitleDeliveryMethod.External); stream.setDeliveryUrl(String.format("%1$s/Videos/%2$s/%3$s/Subtitles/%4$s/0/Stream.JSON", mApplication.getApiClient().getApiUrl(), mCurrentStreamInfo.getItemId(), mCurrentStreamInfo.getMediaSourceId(), StringHelper.ToStringCultureInvariant(stream.getIndex()))); mApplication.getApiClient().getSubtitles(stream.getDeliveryUrl(), new Response<SubtitleTrackInfo>() { @Override public void onResponse(final SubtitleTrackInfo info) { if (info != null) { TvApp.getApplication().getLogger().Debug("Adding json subtitle track to player"); mFragment.addManualSubtitles(info); } else { TvApp.getApplication().getLogger().Error("Empty subtitle result"); Utils.showToast(mApplication, mApplication.getResources().getString(R.string.msg_unable_load_subs)); mFragment.showSubLoadingMsg(false); } } @Override public void onError(Exception ex) { TvApp.getApplication().getLogger().ErrorException("Error downloading subtitles", ex); Utils.showToast(mApplication, mApplication.getResources().getString(R.string.msg_unable_load_subs)); mFragment.showSubLoadingMsg(false); } }); break; case Hls: break; } } } public void pause() { mPlaybackState = PlaybackState.PAUSED; mVideoManager.pause(); if (mFragment != null) { mFragment.setFadingEnabled(false); mFragment.setPlayPauseActionState(ImageButton.STATE_PRIMARY); } stopReportLoop(); // start a slower report for pause state to keep session alive startPauseReportLoop(); } public void playPause() { switch (mPlaybackState) { case PLAYING: pause(); break; case PAUSED: case IDLE: stopReportLoop(); play(getCurrentPosition()); break; } } public void stop() { stopReportLoop(); if (mPlaybackState != PlaybackState.IDLE && mPlaybackState != PlaybackState.UNDEFINED) { mPlaybackState = PlaybackState.IDLE; if (mVideoManager.isPlaying()) mVideoManager.stopPlayback(); //give it a just a beat to actually stop - this keeps it from re-requesting the stream after we tell the server we've stopped try { Thread.sleep(150); } catch (InterruptedException e) { e.printStackTrace(); } Long mbPos = mCurrentPosition * 10000; Utils.ReportStopped(getCurrentlyPlayingItem(), getCurrentStreamInfo(), mbPos); if (!isLiveTv) { // update the actual items resume point getCurrentlyPlayingItem().getUserData().setPlaybackPositionTicks(mbPos); } // be sure to unmute audio in case it was muted TvApp.getApplication().setAudioMuted(false); } } public void next() { mApplication.getLogger().Debug("Next called."); if (mCurrentIndex < mItems.size() - 1) { stop(); mCurrentIndex++; mApplication.getLogger().Debug("Moving to index: " + mCurrentIndex + " out of " + mItems.size() + " total items."); spinnerOff = false; play(0); } } public void prev() { } public void seek(final long pos) { mApplication.getLogger().Debug("Seeking to " + pos); mApplication.getLogger().Debug("Container: "+mCurrentStreamInfo.getContainer()); if (mPlaybackMethod == PlayMethod.Transcode) { //mkv transcodes require re-start of stream for seek mVideoManager.stopPlayback(); mCurrentStreamInfo.setStartPositionTicks(pos * 10000); mVideoManager.setVideoPath(mCurrentStreamInfo.ToUrl(mApplication.getApiClient().getApiUrl(), mApplication.getApiClient().getAccessToken())); mVideoManager.start(); } else { if (mVideoManager.isNativeMode() && "ts".equals(mCurrentStreamInfo.getContainer())) { //Exo does not support seeking in .ts Utils.showToast(TvApp.getApplication(), "Unable to seek"); } else if (mVideoManager.seekTo(pos) >= 0) { if (mFragment != null) { mFragment.updateEndTime(mVideoManager.getDuration() - pos); } } else { Utils.showToast(TvApp.getApplication(), "Unable to seek"); } } } private long currentSkipPos = 0; private Runnable skipRunnable = new Runnable() { @Override public void run() { if (!isPlaying()) return; // in case we completed since this was requested seek(currentSkipPos); currentSkipPos = 0; startReportLoop(); updateProgress = true; // re-enable true progress updates } }; private float playSpeed = 1; public void togglePlaySpeed() { if (playSpeed < 4) playSpeed += 1f; else playSpeed = 1; mVideoManager.setPlaySpeed(playSpeed); } public void skip(int msec) { if (isPlaying()) { mHandler.removeCallbacks(skipRunnable); stopReportLoop(); updateProgress = false; // turn this off so we can show where it will be jumping to currentSkipPos = (currentSkipPos == 0 ? mVideoManager.getCurrentPosition() : currentSkipPos) + msec; if (currentSkipPos < 0) currentSkipPos = 0; mApplication.getLogger().Debug("Duration reported as: "+mVideoManager.getDuration()); if (currentSkipPos > mVideoManager.getDuration()) currentSkipPos = mVideoManager.getDuration() - 1000; mFragment.setCurrentTime(currentSkipPos); mHandler.postDelayed(skipRunnable, 800); } } private void updateTvProgramInfo() { // Get the current program info when playing a live TV channel final BaseItemDto channel = getCurrentlyPlayingItem(); if (channel.getType().equals("TvChannel")) { TvApp.getApplication().getApiClient().GetLiveTvChannelAsync(channel.getId(), TvApp.getApplication().getCurrentUser().getId(), new Response<ChannelInfoDto>() { @Override public void onResponse(ChannelInfoDto response) { BaseItemDto program = response.getCurrentProgram(); if (program != null) { channel.setName(program.getName() + liveTvChannelName); channel.setPremiereDate(program.getStartDate()); channel.setEndDate(program.getEndDate()); channel.setOfficialRating(program.getOfficialRating()); channel.setRunTimeTicks(program.getRunTimeTicks()); mCurrentProgramEndTime = channel.getEndDate() != null ? Utils.convertToLocalDate(channel.getEndDate()).getTime() : 0; mCurrentProgramStartTime = channel.getPremiereDate() != null ? Utils.convertToLocalDate(channel.getPremiereDate()).getTime() : 0; mFragment.updateDisplay(); } } }); } } private long getRealTimeProgress() { return System.currentTimeMillis() - mCurrentProgramStartTime; } private void startReportLoop() { Utils.ReportProgress(getCurrentlyPlayingItem(), getCurrentStreamInfo(), mVideoManager.getCurrentPosition() * 10000, false); mReportLoop = new Runnable() { @Override public void run() { if (mPlaybackState == PlaybackState.PLAYING) { long currentTime = mVideoManager.getCurrentPosition(); Utils.ReportProgress(getCurrentlyPlayingItem(), getCurrentStreamInfo(), currentTime * 10000, false); //Do this next up processing here because every 3 seconds is good enough if (!nextItemReported && hasNextItem() && currentTime >= mNextItemThreshold){ nextItemReported = true; mFragment.nextItemThresholdHit(getNextItem()); } } mApplication.setLastUserInteraction(System.currentTimeMillis()); if (mPlaybackState != PlaybackState.UNDEFINED && mPlaybackState != PlaybackState.IDLE) mHandler.postDelayed(this, REPORT_INTERVAL); } }; mHandler.postDelayed(mReportLoop, REPORT_INTERVAL); } private void startPauseReportLoop() { Utils.ReportProgress(getCurrentlyPlayingItem(), getCurrentStreamInfo(), mVideoManager.getCurrentPosition() * 10000, false); mReportLoop = new Runnable() { @Override public void run() { long currentTime = mVideoManager.getCurrentPosition(); Utils.ReportProgress(getCurrentlyPlayingItem(), getCurrentStreamInfo(), currentTime * 10000, true); mHandler.postDelayed(this, 15000); } }; mHandler.postDelayed(mReportLoop, 15000); } private void stopReportLoop() { if (mHandler != null && mReportLoop != null) { mHandler.removeCallbacks(mReportLoop); } } private void delayedSeek(final long position) { mHandler.post(new Runnable() { @Override public void run() { if (mVideoManager.getDuration() <= 0) { // wait until we have valid duration mHandler.postDelayed(this, 25); } else { // do the seek if (mVideoManager.seekTo(position) < 0) Utils.showToast(TvApp.getApplication(), "Unable to seek"); mPlaybackState = PlaybackState.PLAYING; updateProgress = true; mFragment.updateEndTime(mVideoManager.getDuration() - position); } } }); } public void removePreviousQueueItems() { TvApp.getApplication().setLastVideoQueueChange(System.currentTimeMillis()); if (isLiveTv || !MediaManager.isVideoQueueModified()) { MediaManager.clearVideoQueue(); return; } if (mCurrentIndex < 0) return; for (int i = 0; i < mCurrentIndex; i++) { mItems.remove(0); } //Now - look at last item played and, if beyond default resume point, remove it too Long duration = mCurrentStreamInfo != null ? mCurrentStreamInfo.getRunTimeTicks() : null; if (duration != null && mItems.size() > 0) { if (duration < 300000 || mCurrentPosition * 10000 > Math.floor(.90 * duration)) mItems.remove(0); } else if (duration == null) mItems.remove(0); } private void itemComplete() { mPlaybackState = PlaybackState.IDLE; stopReportLoop(); Long mbPos = mVideoManager.getCurrentPosition() * 10000; Utils.ReportStopped(getCurrentlyPlayingItem(), getCurrentStreamInfo(), mbPos); if (mCurrentIndex < mItems.size() - 1) { // move to next in queue mCurrentIndex++; mApplication.getLogger().Debug("Moving to next queue item. Index: "+mCurrentIndex); spinnerOff = false; play(0); } else { // exit activity mApplication.getLogger().Debug("Last item completed. Finishing activity."); mFragment.finish(); } } private long lastProgressPosition; private boolean isRestart = false; private void setupCallbacks() { mVideoManager.setOnErrorListener(new PlaybackListener() { @Override public void onEvent() { if (isLiveTv && mApplication.directStreamLiveTv()) { Utils.showToast(mApplication, mApplication.getString(R.string.msg_error_live_stream)); mApplication.setDirectStreamLiveTv(false); Utils.retrieveAndPlay(getCurrentlyPlayingItem().getId(), false, mApplication); mFragment.finish(); } else { String msg = mApplication.getString(R.string.video_error_unknown_error); Utils.showToast(mApplication, mApplication.getString(R.string.msg_video_playback_error) + msg); mApplication.getLogger().Error("Playback error - " + msg); mPlaybackState = PlaybackState.ERROR; stop(); } } }); mVideoManager.setOnPreparedListener(new PlaybackListener() { @Override public void onEvent() { if (mPlaybackState == PlaybackState.BUFFERING) { mPlaybackState = PlaybackState.PLAYING; mFragment.updateEndTime(isLiveTv && getCurrentlyPlayingItem().getEndDate() != null ? Utils.convertToLocalDate(getCurrentlyPlayingItem().getEndDate()).getTime() - System.currentTimeMillis() : mVideoManager.getDuration() - mStartPosition); startReportLoop(); } TvApp.getApplication().getLogger().Info("Play method: ", mCurrentStreamInfo.getPlayMethod() == PlayMethod.Transcode ? "Trans" : "Direct"); if (mPlaybackState == PlaybackState.PAUSED) { mPlaybackState = PlaybackState.PLAYING; } else { if (mDefaultSubIndex >= 0) { //Default subs requested select them mApplication.getLogger().Info("Selecting default sub stream: " + mDefaultSubIndex); switchSubtitleStream(mDefaultSubIndex); } else { TvApp.getApplication().getLogger().Info("Turning off subs by default"); mVideoManager.disableSubs(); } if (!mVideoManager.isNativeMode() && mDefaultAudioIndex >= 0) { TvApp.getApplication().getLogger().Info("Selecting default audio stream: " + mDefaultAudioIndex); switchAudioStream(mDefaultAudioIndex); } } } }); mVideoManager.setOnProgressListener(new PlaybackListener() { @Override public void onEvent() { if (isPlaying() && updateProgress) { updateProgress = false; if (isLiveTv && mVideoManager.isNativeMode() && lastProgressPosition > 0 && lastProgressPosition == mVideoManager.getCurrentPosition()) { mApplication.getLogger().Debug("************** playback appears to have stalled - attempting re-start"); mVideoManager.stopPlayback(); mPlaybackState = PlaybackState.IDLE; isRestart = true; play(0); } else { lastProgressPosition = mVideoManager.getCurrentPosition(); //mApplication.getLogger().Debug("******* progress listener fired"); } boolean continueUpdate = true; if (!spinnerOff) { if (mStartPosition > 0) { if (mPlaybackMethod == PlayMethod.Transcode) { // we started the stream at seek point mStartPosition = 0; } else { mPlaybackState = PlaybackState.SEEKING; delayedSeek(mStartPosition); continueUpdate = false; mStartPosition = 0; } } else { stopSpinner(); } if (getPlaybackMethod() != PlayMethod.Transcode) { if (mCurrentOptions.getAudioStreamIndex() != null) mVideoManager.setAudioTrack(mCurrentOptions.getAudioStreamIndex()); } } if (continueUpdate) { mApplication.setLastUserInteraction(System.currentTimeMillis()); // don't want to auto logoff during playback if (isLiveTv && mCurrentProgramEndTime > 0 && System.currentTimeMillis() >= mCurrentProgramEndTime) { // crossed fire off an async routine to update the program info updateTvProgramInfo(); } final Long currentTime = isLiveTv && mCurrentProgramStartTime > 0 ? getRealTimeProgress() : mVideoManager.getCurrentPosition(); mFragment.setCurrentTime(currentTime); mCurrentPosition = currentTime; mFragment.updateSubtitles(currentTime); } updateProgress = continueUpdate; } } }); mVideoManager.setOnCompletionListener(new PlaybackListener() { @Override public void onEvent() { TvApp.getApplication().getLogger().Debug("On Completion fired"); itemComplete(); } }); } public long getCurrentPosition() { return mCurrentPosition; } public boolean isPaused() { return mPlaybackState == PlaybackState.PAUSED; } public boolean isIdle() { return mPlaybackState == PlaybackState.IDLE; } public int getZoomMode() { return mVideoManager.getZoomMode(); } public void setZoom(int mode) { mVideoManager.setZoom(mode); } /* * List of various states that we can be in */ public enum PlaybackState { PLAYING, PAUSED, BUFFERING, IDLE, SEEKING, UNDEFINED, ERROR; } }