1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.tv.dvr.ui.playback; 18 19 import android.media.PlaybackParams; 20 import android.media.session.PlaybackState; 21 import android.media.tv.TvContentRating; 22 import android.media.tv.TvInputManager; 23 import android.media.tv.TvTrackInfo; 24 import android.media.tv.TvView; 25 import android.text.TextUtils; 26 import android.util.Log; 27 28 import com.android.tv.dvr.data.RecordedProgram; 29 30 import java.util.ArrayList; 31 import java.util.List; 32 import java.util.concurrent.TimeUnit; 33 34 class DvrPlayer { 35 private static final String TAG = "DvrPlayer"; 36 private static final boolean DEBUG = false; 37 38 /** 39 * The max rewinding speed supported by DVR player. 40 */ 41 public static final int MAX_REWIND_SPEED = 256; 42 /** 43 * The max fast-forwarding speed supported by DVR player. 44 */ 45 public static final int MAX_FAST_FORWARD_SPEED = 256; 46 47 private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2); 48 private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826 49 50 private RecordedProgram mProgram; 51 private long mInitialSeekPositionMs; 52 private final TvView mTvView; 53 private DvrPlayerCallback mCallback; 54 private OnAspectRatioChangedListener mOnAspectRatioChangedListener; 55 private OnContentBlockedListener mOnContentBlockedListener; 56 private OnTracksAvailabilityChangedListener mOnTracksAvailabilityChangedListener; 57 private OnTrackSelectedListener mOnAudioTrackSelectedListener; 58 private OnTrackSelectedListener mOnSubtitleTrackSelectedListener; 59 private String mSelectedAudioTrackId; 60 private String mSelectedSubtitleTrackId; 61 private float mAspectRatio = Float.NaN; 62 private int mPlaybackState = PlaybackState.STATE_NONE; 63 private long mTimeShiftCurrentPositionMs; 64 private boolean mPauseOnPrepared; 65 private boolean mHasClosedCaption; 66 private boolean mHasMultiAudio; 67 private final PlaybackParams mPlaybackParams = new PlaybackParams(); 68 private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback(); 69 private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; 70 private boolean mTimeShiftPlayAvailable; 71 72 public static class DvrPlayerCallback { 73 /** 74 * Called when the playback position is changed. The normal updating frequency is 75 * around 1 sec., which is restricted to the implementation of 76 * {@link android.media.tv.TvInputService}. 77 */ onPlaybackPositionChanged(long positionMs)78 public void onPlaybackPositionChanged(long positionMs) { } 79 /** 80 * Called when the playback state or the playback speed is changed. 81 */ onPlaybackStateChanged(int playbackState, int playbackSpeed)82 public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { } 83 /** 84 * Called when the playback toward the end. 85 */ onPlaybackEnded()86 public void onPlaybackEnded() { } 87 } 88 89 public interface OnAspectRatioChangedListener { 90 /** 91 * Called when the Video's aspect ratio is changed. 92 * 93 * @param videoAspectRatio The aspect ratio of video. 0 stands for unknown ratios. 94 * Listeners should handle it carefully. 95 */ onAspectRatioChanged(float videoAspectRatio)96 void onAspectRatioChanged(float videoAspectRatio); 97 } 98 99 public interface OnContentBlockedListener { 100 /** 101 * Called when the Video's aspect ratio is changed. 102 */ onContentBlocked(TvContentRating rating)103 void onContentBlocked(TvContentRating rating); 104 } 105 106 public interface OnTracksAvailabilityChangedListener { 107 /** 108 * Called when the Video's subtitle or audio tracks are changed. 109 */ onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio)110 void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio); 111 } 112 113 public interface OnTrackSelectedListener { 114 /** 115 * Called when certain subtitle or audio track is selected. 116 */ onTrackSelected(String selectedTrackId)117 void onTrackSelected(String selectedTrackId); 118 } 119 DvrPlayer(TvView tvView)120 public DvrPlayer(TvView tvView) { 121 mTvView = tvView; 122 mTvView.setCaptionEnabled(true); 123 mPlaybackParams.setSpeed(1.0f); 124 setTvViewCallbacks(); 125 setCallback(null); 126 } 127 128 /** 129 * Prepares playback. 130 * 131 * @param doPlay indicates DVR player do or do not start playback after media is prepared. 132 */ prepare(boolean doPlay)133 public void prepare(boolean doPlay) throws IllegalStateException { 134 if (DEBUG) Log.d(TAG, "prepare()"); 135 if (mProgram == null) { 136 throw new IllegalStateException("Recorded program not set"); 137 } else if (mPlaybackState != PlaybackState.STATE_NONE) { 138 throw new IllegalStateException("Playback is already prepared"); 139 } 140 mTvView.timeShiftPlay(mProgram.getInputId(), mProgram.getUri()); 141 mPlaybackState = PlaybackState.STATE_CONNECTING; 142 mPauseOnPrepared = !doPlay; 143 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 144 } 145 146 /** 147 * Resumes playback. 148 */ play()149 public void play() throws IllegalStateException { 150 if (DEBUG) Log.d(TAG, "play()"); 151 if (!isPlaybackPrepared()) { 152 throw new IllegalStateException("Recorded program not set or video not ready yet"); 153 } 154 switch (mPlaybackState) { 155 case PlaybackState.STATE_FAST_FORWARDING: 156 case PlaybackState.STATE_REWINDING: 157 setPlaybackSpeed(1); 158 break; 159 default: 160 mTvView.timeShiftResume(); 161 } 162 mPlaybackState = PlaybackState.STATE_PLAYING; 163 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 164 } 165 166 /** 167 * Pauses playback. 168 */ pause()169 public void pause() throws IllegalStateException { 170 if (DEBUG) Log.d(TAG, "pause()"); 171 if (!isPlaybackPrepared()) { 172 throw new IllegalStateException("Recorded program not set or playback not started yet"); 173 } 174 switch (mPlaybackState) { 175 case PlaybackState.STATE_FAST_FORWARDING: 176 case PlaybackState.STATE_REWINDING: 177 setPlaybackSpeed(1); 178 // falls through 179 case PlaybackState.STATE_PLAYING: 180 mTvView.timeShiftPause(); 181 mPlaybackState = PlaybackState.STATE_PAUSED; 182 break; 183 default: 184 break; 185 } 186 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 187 } 188 189 /** 190 * Fast-forwards playback with the given speed. If the given speed is larger than 191 * {@value #MAX_FAST_FORWARD_SPEED}, uses {@value #MAX_FAST_FORWARD_SPEED}. 192 */ fastForward(int speed)193 public void fastForward(int speed) throws IllegalStateException { 194 if (DEBUG) Log.d(TAG, "fastForward()"); 195 if (!isPlaybackPrepared()) { 196 throw new IllegalStateException("Recorded program not set or playback not started yet"); 197 } 198 if (speed <= 0) { 199 throw new IllegalArgumentException("Speed cannot be negative or 0"); 200 } 201 if (mTimeShiftCurrentPositionMs >= mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS) { 202 return; 203 } 204 speed = Math.min(speed, MAX_FAST_FORWARD_SPEED); 205 if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); 206 setPlaybackSpeed(speed); 207 mPlaybackState = PlaybackState.STATE_FAST_FORWARDING; 208 mCallback.onPlaybackStateChanged(mPlaybackState, speed); 209 } 210 211 /** 212 * Rewinds playback with the given speed. If the given speed is larger than 213 * {@value #MAX_REWIND_SPEED}, uses {@value #MAX_REWIND_SPEED}. 214 */ rewind(int speed)215 public void rewind(int speed) throws IllegalStateException { 216 if (DEBUG) Log.d(TAG, "rewind()"); 217 if (!isPlaybackPrepared()) { 218 throw new IllegalStateException("Recorded program not set or playback not started yet"); 219 } 220 if (speed <= 0) { 221 throw new IllegalArgumentException("Speed cannot be negative or 0"); 222 } 223 if (mTimeShiftCurrentPositionMs <= REWIND_POSITION_MARGIN_MS) { 224 return; 225 } 226 speed = Math.min(speed, MAX_REWIND_SPEED); 227 if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); 228 setPlaybackSpeed(-speed); 229 mPlaybackState = PlaybackState.STATE_REWINDING; 230 mCallback.onPlaybackStateChanged(mPlaybackState, speed); 231 } 232 233 /** 234 * Seeks playback to the specified position. 235 */ seekTo(long positionMs)236 public void seekTo(long positionMs) throws IllegalStateException { 237 if (DEBUG) Log.d(TAG, "seekTo()"); 238 if (!isPlaybackPrepared()) { 239 throw new IllegalStateException("Recorded program not set or playback not started yet"); 240 } 241 if (mProgram == null || mPlaybackState == PlaybackState.STATE_NONE) { 242 return; 243 } 244 positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS); 245 if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs); 246 mTvView.timeShiftSeekTo(positionMs + mStartPositionMs); 247 if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING || 248 mPlaybackState == PlaybackState.STATE_REWINDING) { 249 mPlaybackState = PlaybackState.STATE_PLAYING; 250 mTvView.timeShiftResume(); 251 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 252 } 253 } 254 255 /** 256 * Resets playback. 257 */ reset()258 public void reset() { 259 if (DEBUG) Log.d(TAG, "reset()"); 260 mCallback.onPlaybackStateChanged(PlaybackState.STATE_NONE, 1); 261 mPlaybackState = PlaybackState.STATE_NONE; 262 mTvView.reset(); 263 mTimeShiftPlayAvailable = false; 264 mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; 265 mTimeShiftCurrentPositionMs = 0; 266 mPlaybackParams.setSpeed(1.0f); 267 mProgram = null; 268 mSelectedAudioTrackId = null; 269 mSelectedSubtitleTrackId = null; 270 } 271 272 /** 273 * Sets callbacks for playback. 274 */ setCallback(DvrPlayerCallback callback)275 public void setCallback(DvrPlayerCallback callback) { 276 if (callback != null) { 277 mCallback = callback; 278 } else { 279 mCallback = mEmptyCallback; 280 } 281 } 282 283 /** 284 * Sets the listener to aspect ratio changing. 285 */ setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener)286 public void setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener) { 287 mOnAspectRatioChangedListener = listener; 288 } 289 290 /** 291 * Sets the listener to content blocking. 292 */ setOnContentBlockedListener(OnContentBlockedListener listener)293 public void setOnContentBlockedListener(OnContentBlockedListener listener) { 294 mOnContentBlockedListener = listener; 295 } 296 297 /** 298 * Sets the listener to tracks changing. 299 */ setOnTracksAvailabilityChangedListener( OnTracksAvailabilityChangedListener listener)300 public void setOnTracksAvailabilityChangedListener( 301 OnTracksAvailabilityChangedListener listener) { 302 mOnTracksAvailabilityChangedListener = listener; 303 } 304 305 /** 306 * Sets the listener to tracks of the given type being selected. 307 * 308 * @param trackType should be either {@link TvTrackInfo#TYPE_AUDIO} 309 * or {@link TvTrackInfo#TYPE_SUBTITLE}. 310 */ setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener)311 public void setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener) { 312 if (trackType == TvTrackInfo.TYPE_AUDIO) { 313 mOnAudioTrackSelectedListener = listener; 314 } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { 315 mOnSubtitleTrackSelectedListener = listener; 316 } 317 } 318 319 /** 320 * Gets the listener to tracks of the given type being selected. 321 */ getOnTrackSelectedListener(int trackType)322 public OnTrackSelectedListener getOnTrackSelectedListener(int trackType) { 323 if (trackType == TvTrackInfo.TYPE_AUDIO) { 324 return mOnAudioTrackSelectedListener; 325 } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { 326 return mOnSubtitleTrackSelectedListener; 327 } 328 return null; 329 } 330 331 /** 332 * Sets recorded programs for playback. If the player is playing another program, stops it. 333 */ setProgram(RecordedProgram program, long initialSeekPositionMs)334 public void setProgram(RecordedProgram program, long initialSeekPositionMs) { 335 if (mProgram != null && mProgram.equals(program)) { 336 return; 337 } 338 if (mPlaybackState != PlaybackState.STATE_NONE) { 339 reset(); 340 } 341 mInitialSeekPositionMs = initialSeekPositionMs; 342 mProgram = program; 343 } 344 345 /** 346 * Returns the recorded program now playing. 347 */ getProgram()348 public RecordedProgram getProgram() { 349 return mProgram; 350 } 351 352 /** 353 * Returns the currrent playback posistion in msecs. 354 */ getPlaybackPosition()355 public long getPlaybackPosition() { 356 return mTimeShiftCurrentPositionMs; 357 } 358 359 /** 360 * Returns the playback speed currently used. 361 */ getPlaybackSpeed()362 public int getPlaybackSpeed() { 363 return (int) mPlaybackParams.getSpeed(); 364 } 365 366 /** 367 * Returns the playback state defined in {@link android.media.session.PlaybackState}. 368 */ getPlaybackState()369 public int getPlaybackState() { 370 return mPlaybackState; 371 } 372 373 /** 374 * Returns the subtitle tracks of the current playback. 375 */ getSubtitleTracks()376 public ArrayList<TvTrackInfo> getSubtitleTracks() { 377 return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE)); 378 } 379 380 /** 381 * Returns the audio tracks of the current playback. 382 */ getAudioTracks()383 public ArrayList<TvTrackInfo> getAudioTracks() { 384 return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO)); 385 } 386 387 /** 388 * Returns the ID of the selected track of the given type. 389 */ getSelectedTrackId(int trackType)390 public String getSelectedTrackId(int trackType) { 391 if (trackType == TvTrackInfo.TYPE_AUDIO) { 392 return mSelectedAudioTrackId; 393 } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { 394 return mSelectedSubtitleTrackId; 395 } 396 return null; 397 } 398 399 /** 400 * Returns if playback of the recorded program is started. 401 */ isPlaybackPrepared()402 public boolean isPlaybackPrepared() { 403 return mPlaybackState != PlaybackState.STATE_NONE 404 && mPlaybackState != PlaybackState.STATE_CONNECTING; 405 } 406 407 /** 408 * Selects the given track. 409 * 410 * @return ID of the selected track. 411 */ selectTrack(int trackType, TvTrackInfo selectedTrack)412 String selectTrack(int trackType, TvTrackInfo selectedTrack) { 413 String oldSelectedTrackId = getSelectedTrackId(trackType); 414 String newSelectedTrackId = selectedTrack == null ? null : selectedTrack.getId(); 415 if (!TextUtils.equals(oldSelectedTrackId, newSelectedTrackId)) { 416 if (selectedTrack == null) { 417 mTvView.selectTrack(trackType, null); 418 return null; 419 } else { 420 List<TvTrackInfo> tracks = mTvView.getTracks(trackType); 421 if (tracks != null && tracks.contains(selectedTrack)) { 422 mTvView.selectTrack(trackType, newSelectedTrackId); 423 return newSelectedTrackId; 424 } else if (trackType == TvTrackInfo.TYPE_SUBTITLE && oldSelectedTrackId != null) { 425 // Track not found, disabled closed caption. 426 mTvView.selectTrack(trackType, null); 427 return null; 428 } 429 } 430 } 431 return oldSelectedTrackId; 432 } 433 setSelectedTrackId(int trackType, String trackId)434 private void setSelectedTrackId(int trackType, String trackId) { 435 if (trackType == TvTrackInfo.TYPE_AUDIO) { 436 mSelectedAudioTrackId = trackId; 437 } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { 438 mSelectedSubtitleTrackId = trackId; 439 } 440 } 441 setPlaybackSpeed(int speed)442 private void setPlaybackSpeed(int speed) { 443 mPlaybackParams.setSpeed(speed); 444 mTvView.timeShiftSetPlaybackParams(mPlaybackParams); 445 } 446 getRealSeekPosition(long seekPositionMs, long endMarginMs)447 private long getRealSeekPosition(long seekPositionMs, long endMarginMs) { 448 return Math.max(0, Math.min(seekPositionMs, mProgram.getDurationMillis() - endMarginMs)); 449 } 450 setTvViewCallbacks()451 private void setTvViewCallbacks() { 452 mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { 453 @Override 454 public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { 455 if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs); 456 mStartPositionMs = timeMs; 457 if (mTimeShiftPlayAvailable) { 458 resumeToWatchedPositionIfNeeded(); 459 } 460 } 461 462 @Override 463 public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { 464 if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs); 465 if (!mTimeShiftPlayAvailable) { 466 // Workaround of b/31436263 467 return; 468 } 469 // Workaround of b/32211561, TIF won't report start position when TIS report 470 // its start position as 0. In that case, we have to do the prework of playback 471 // on the first time we get current position, and the start position should be 0 472 // at that time. 473 if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) { 474 mStartPositionMs = 0; 475 resumeToWatchedPositionIfNeeded(); 476 } 477 timeMs -= mStartPositionMs; 478 if (mPlaybackState == PlaybackState.STATE_REWINDING 479 && timeMs <= REWIND_POSITION_MARGIN_MS) { 480 play(); 481 } else { 482 mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); 483 mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); 484 if (timeMs >= mProgram.getDurationMillis()) { 485 pause(); 486 mCallback.onPlaybackEnded(); 487 } 488 } 489 } 490 }); 491 mTvView.setCallback(new TvView.TvInputCallback() { 492 @Override 493 public void onTimeShiftStatusChanged(String inputId, int status) { 494 if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); 495 if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE 496 && mPlaybackState == PlaybackState.STATE_CONNECTING) { 497 mTimeShiftPlayAvailable = true; 498 if (mStartPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { 499 // onTimeShiftStatusChanged is sometimes called after 500 // onTimeShiftStartPositionChanged is called. In this case, 501 // resumeToWatchedPositionIfNeeded needs to be called here. 502 resumeToWatchedPositionIfNeeded(); 503 } 504 } 505 } 506 507 @Override 508 public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { 509 boolean hasClosedCaption = 510 !mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE).isEmpty(); 511 boolean hasMultiAudio = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO).size() > 1; 512 if ((hasClosedCaption != mHasClosedCaption || hasMultiAudio != mHasMultiAudio) 513 && mOnTracksAvailabilityChangedListener != null) { 514 mOnTracksAvailabilityChangedListener 515 .onTracksAvailabilityChanged(hasClosedCaption, hasMultiAudio); 516 } 517 mHasClosedCaption = hasClosedCaption; 518 mHasMultiAudio = hasMultiAudio; 519 } 520 521 @Override 522 public void onTrackSelected(String inputId, int type, String trackId) { 523 if (type == TvTrackInfo.TYPE_AUDIO || type == TvTrackInfo.TYPE_SUBTITLE) { 524 setSelectedTrackId(type, trackId); 525 OnTrackSelectedListener listener = getOnTrackSelectedListener(type); 526 if (listener != null) { 527 listener.onTrackSelected(trackId); 528 } 529 } else if (type == TvTrackInfo.TYPE_VIDEO && trackId != null 530 && mOnAspectRatioChangedListener != null) { 531 List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); 532 if (trackInfos != null) { 533 for (TvTrackInfo trackInfo : trackInfos) { 534 if (trackInfo.getId().equals(trackId)) { 535 float videoAspectRatio; 536 int videoWidth = trackInfo.getVideoWidth(); 537 int videoHeight = trackInfo.getVideoHeight(); 538 if (videoWidth > 0 && videoHeight > 0) { 539 videoAspectRatio = trackInfo.getVideoPixelAspectRatio() 540 * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); 541 } else { 542 // Aspect ratio is unknown. Pass the message to listeners. 543 videoAspectRatio = 0; 544 } 545 if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); 546 if (mAspectRatio != videoAspectRatio || videoAspectRatio == 0) { 547 mOnAspectRatioChangedListener 548 .onAspectRatioChanged(videoAspectRatio); 549 mAspectRatio = videoAspectRatio; 550 return; 551 } 552 } 553 } 554 } 555 } 556 } 557 558 @Override 559 public void onContentBlocked(String inputId, TvContentRating rating) { 560 if (mOnContentBlockedListener != null) { 561 mOnContentBlockedListener.onContentBlocked(rating); 562 } 563 } 564 }); 565 } 566 resumeToWatchedPositionIfNeeded()567 private void resumeToWatchedPositionIfNeeded() { 568 if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { 569 mTvView.timeShiftSeekTo(getRealSeekPosition(mInitialSeekPositionMs, 570 SEEK_POSITION_MARGIN_MS) + mStartPositionMs); 571 mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; 572 } 573 if (mPauseOnPrepared) { 574 mTvView.timeShiftPause(); 575 mPlaybackState = PlaybackState.STATE_PAUSED; 576 mPauseOnPrepared = false; 577 } else { 578 mTvView.timeShiftResume(); 579 mPlaybackState = PlaybackState.STATE_PLAYING; 580 } 581 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 582 } 583 }