AddressedMediaPlayer.java revision 0533d66ae0a334789c197cde425d3ad87a84dd7d
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 17package com.android.bluetooth.avrcp; 18 19import android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.bluetooth.BluetoothAvrcp; 22import android.media.session.MediaSession; 23import android.media.session.PlaybackState; 24import android.media.session.MediaSession.QueueItem; 25import android.media.MediaDescription; 26import android.media.MediaMetadata; 27import android.os.Bundle; 28import android.util.Log; 29 30import com.android.bluetooth.Utils; 31 32import java.nio.ByteBuffer; 33import java.util.List; 34import java.util.Arrays; 35import java.util.ArrayList; 36 37/************************************************************************************************* 38 * Provides functionality required for Addressed Media Player, like Now Playing List related 39 * browsing commands, control commands to the current addressed player(playItem, play, pause, etc) 40 * Acts as an Interface to communicate with media controller APIs for NowPlayingItems. 41 ************************************************************************************************/ 42 43public class AddressedMediaPlayer { 44 static private final String TAG = "AddressedMediaPlayer"; 45 static private final Boolean DEBUG = false; 46 47 static private final long SINGLE_QID = 1; 48 static private final String UNKNOWN_TITLE = "(unknown)"; 49 50 private AvrcpMediaRspInterface mMediaInterface; 51 private List<MediaSession.QueueItem> mNowPlayingList; 52 53 private final List<MediaSession.QueueItem> mUnknownNowPlayingList; 54 55 private long mLastTrackIdSent; 56 57 public AddressedMediaPlayer(AvrcpMediaRspInterface mediaInterface) { 58 mNowPlayingList = null; 59 mMediaInterface = mediaInterface; 60 mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID; 61 List<MediaSession.QueueItem> unknown = new ArrayList<MediaSession.QueueItem>(); 62 unknown.add(getCurrentQueueItem(null, SINGLE_QID)); 63 mUnknownNowPlayingList = unknown; 64 } 65 66 void cleanup() { 67 if (DEBUG) Log.v(TAG, "cleanup"); 68 mNowPlayingList = null; 69 mMediaInterface = null; 70 mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID; 71 } 72 73 /* get now playing list from addressed player */ 74 void getFolderItemsNowPlaying(byte[] bdaddr, AvrcpCmd.FolderItemsCmd reqObj, 75 @Nullable MediaController mediaController) { 76 if (DEBUG) Log.v(TAG, "getFolderItemsNowPlaying"); 77 if (mediaController == null) { 78 // No players (if a player exists, we would have selected it) 79 Log.e(TAG, "mediaController = null, sending no available players response"); 80 mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_AVBL_PLAY, null); 81 return; 82 } 83 List<MediaSession.QueueItem> items = getNowPlayingList(mediaController); 84 getFolderItemsFilterAttr(bdaddr, reqObj, items, AvrcpConstants.BTRC_SCOPE_NOW_PLAYING, 85 reqObj.mStartItem, reqObj.mEndItem, mediaController); 86 } 87 88 /* get item attributes for item in now playing list */ 89 void getItemAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd itemAttr, 90 @Nullable MediaController mediaController) { 91 int status = AvrcpConstants.RSP_NO_ERROR; 92 long mediaId = ByteBuffer.wrap(itemAttr.mUid).getLong(); 93 List<MediaSession.QueueItem> items = getNowPlayingList(mediaController); 94 95 /* checking if item attributes has been asked for now playing item or 96 * some other item with specific media id */ 97 if (Arrays.equals(itemAttr.mUid, AvrcpConstants.TRACK_IS_SELECTED)) { 98 if (DEBUG) Log.d(TAG, "getItemAttr: Remote requests for now playing contents:"); 99 100 // get the current playing metadata and send. 101 getItemAttrFilterAttr(bdaddr, itemAttr, getCurrentQueueItem(mediaController, mediaId), 102 mediaController); 103 return; 104 } 105 106 if (DEBUG) Log.d(TAG, "getItemAttr-UID: 0x" + Utils.byteArrayToString(itemAttr.mUid)); 107 for (MediaSession.QueueItem item : items) { 108 if (item.getQueueId() == mediaId) { 109 getItemAttrFilterAttr(bdaddr, itemAttr, item, mediaController); 110 return; 111 } 112 } 113 114 // Couldn't find it, so the id is invalid 115 mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM, null); 116 } 117 118 /* Refresh and get the queue of now playing. 119 */ 120 private List<MediaSession.QueueItem> getNowPlayingList( 121 @Nullable MediaController mediaController) { 122 if (mediaController == null) return mUnknownNowPlayingList; 123 if (mNowPlayingList != null) return mNowPlayingList; 124 List<MediaSession.QueueItem> items = mediaController.getQueue(); 125 if (items == null) { 126 Log.i(TAG, "null queue from " + mediaController.getPackageName() 127 + ", constructing current-item list"); 128 MediaMetadata metadata = mediaController.getMetadata(); 129 // Because we are database-unaware, we can just number the item here whatever we want 130 // because they have to re-poll it every time. 131 MediaSession.QueueItem current = getCurrentQueueItem(mediaController, SINGLE_QID); 132 items = new ArrayList<MediaSession.QueueItem>(); 133 items.add(current); 134 } 135 mNowPlayingList = items; 136 return items; 137 } 138 139 /* Constructs a queue item representing the current playing metadata from an 140 * active controller with queue id |qid|. 141 */ 142 private MediaSession.QueueItem getCurrentQueueItem( 143 @Nullable MediaController controller, long qid) { 144 if (controller == null) { 145 MediaDescription.Builder bob = new MediaDescription.Builder(); 146 bob.setTitle(UNKNOWN_TITLE); 147 return new QueueItem(bob.build(), qid); 148 } 149 150 MediaMetadata metadata = controller.getMetadata(); 151 if (metadata == null) { 152 Log.w(TAG, "Controller has no metadata!? Making an empty one"); 153 metadata = (new MediaMetadata.Builder()).build(); 154 } 155 156 MediaDescription.Builder bob = new MediaDescription.Builder(); 157 MediaDescription desc = metadata.getDescription(); 158 159 // set the simple ones that MediaMetadata builds for us 160 bob.setMediaId(desc.getMediaId()); 161 bob.setTitle(desc.getTitle()); 162 bob.setSubtitle(desc.getSubtitle()); 163 bob.setDescription(desc.getDescription()); 164 // fill the ones that we use later 165 bob.setExtras(fillBundle(metadata, desc.getExtras())); 166 167 // build queue item with the new metadata 168 desc = bob.build(); 169 return new QueueItem(desc, qid); 170 } 171 172 private Bundle fillBundle(MediaMetadata metadata, Bundle currentExtras) { 173 if (metadata == null) { 174 return currentExtras; 175 } 176 177 String[] stringKeys = {MediaMetadata.METADATA_KEY_ARTIST, MediaMetadata.METADATA_KEY_ALBUM, 178 MediaMetadata.METADATA_KEY_GENRE}; 179 String[] longKeys = {MediaMetadata.METADATA_KEY_TRACK_NUMBER, 180 MediaMetadata.METADATA_KEY_NUM_TRACKS, MediaMetadata.METADATA_KEY_DURATION}; 181 182 Bundle bundle = currentExtras; 183 if (bundle == null) bundle = new Bundle(); 184 185 for (String key : stringKeys) { 186 String current = bundle.getString(key); 187 if (current == null) bundle.putString(key, metadata.getString(key)); 188 } 189 for (String key : longKeys) { 190 if (!bundle.containsKey(key)) bundle.putLong(key, metadata.getLong(key)); 191 } 192 return bundle; 193 } 194 195 void updateNowPlayingList(List<MediaSession.QueueItem> queue){ 196 mNowPlayingList = queue; 197 } 198 199 /* Instructs media player to play particular media item */ 200 void playItem(byte[] bdaddr, byte[] uid, @Nullable MediaController mediaController) { 201 long qid = ByteBuffer.wrap(uid).getLong(); 202 List<MediaSession.QueueItem> items = mNowPlayingList; 203 204 if (mediaController == null) { 205 Log.e(TAG, "No mediaController when PlayItem " + qid + " requested"); 206 mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR); 207 return; 208 } 209 210 MediaController.TransportControls mediaControllerCntrl = 211 mediaController.getTransportControls(); 212 213 if (items == null) { 214 Log.w(TAG, "nowPlayingItems is null"); 215 mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR); 216 return; 217 } 218 219 for (MediaSession.QueueItem item : items) { 220 if (qid == item.getQueueId()) { 221 if (DEBUG) Log.d(TAG, "Skipping to ID " + qid); 222 mediaControllerCntrl.skipToQueueItem(qid); 223 mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR); 224 return; 225 } 226 } 227 228 Log.w(TAG, "Invalid now playing Queue ID " + qid); 229 mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM); 230 } 231 232 void getTotalNumOfItems(byte[] bdaddr, @Nullable MediaController mediaController) { 233 if (DEBUG) Log.d(TAG, "getTotalNumOfItems"); 234 List<MediaSession.QueueItem> items = mNowPlayingList; 235 if (items != null) { 236 // We already have the cached list, send the response to remote 237 mMediaInterface.getTotalNumOfItemsRsp( 238 bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, items.size()); 239 return; 240 } 241 242 if (mediaController == null) { 243 Log.e(TAG, "getTotalNumOfItems with no mediaController, sending no items"); 244 mMediaInterface.getTotalNumOfItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, 0); 245 return; 246 } 247 248 // We don't have the cached list, fetch it from Media Controller 249 items = mediaController.getQueue(); 250 if (items == null) { 251 // We may be presenting a queue with only 1 item (the current one) 252 int count = mediaController.getMetadata() != null ? 1 : 0; 253 mMediaInterface.getTotalNumOfItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, count); 254 } 255 // Cache the response for later 256 mNowPlayingList = items; 257 mMediaInterface.getTotalNumOfItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, items.size()); 258 } 259 260 boolean sendTrackChangeWithId(boolean requesting, @Nullable MediaController mediaController) { 261 if (DEBUG) 262 Log.d(TAG, "sendTrackChangeWithId (" + requesting + "): controller " + mediaController); 263 byte[] track; 264 long qid = MediaSession.QueueItem.UNKNOWN_ID; 265 if (mediaController != null) { 266 PlaybackState state = mediaController.getPlaybackState(); 267 /* for any item associated with NowPlaying, uid is queueId */ 268 if (state != null) qid = state.getActiveQueueItemId(); 269 } 270 track = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array(); 271 if (DEBUG) Log.d(TAG, "trackChangedRsp: 0x" + Utils.byteArrayToString(track)); 272 int trackChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED; 273 if (requesting) trackChangedNT = AvrcpConstants.NOTIFICATION_TYPE_INTERIM; 274 mMediaInterface.trackChangedRsp(trackChangedNT, track); 275 mLastTrackIdSent = qid; 276 return (trackChangedNT == AvrcpConstants.NOTIFICATION_TYPE_CHANGED); 277 } 278 279 /* 280 * helper method to check if startItem and endItem index is with range of 281 * MediaItem list. (Resultset containing all items in current path) 282 */ 283 private List<MediaSession.QueueItem> checkIndexOutofBounds( 284 byte[] bdaddr, List<MediaSession.QueueItem> items, long startItem, long endItem) { 285 if (endItem > items.size()) endItem = items.size() - 1; 286 if (startItem > Integer.MAX_VALUE) startItem = Integer.MAX_VALUE; 287 try { 288 List<MediaSession.QueueItem> selected = 289 items.subList((int) startItem, (int) Math.min(items.size(), endItem + 1)); 290 if (selected.isEmpty()) { 291 Log.i(TAG, "itemsSubList is empty."); 292 return null; 293 } 294 return selected; 295 } catch (IndexOutOfBoundsException ex) { 296 Log.i(TAG, "Range (" + startItem + ", " + endItem + ") invalid"); 297 } catch (IllegalArgumentException ex) { 298 Log.i(TAG, "Range start " + startItem + " > size (" + items.size() + ")"); 299 } 300 return null; 301 } 302 303 /* 304 * helper method to filter required attibutes before sending GetFolderItems 305 * response 306 */ 307 private void getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd folderItemsReqObj, 308 List<MediaSession.QueueItem> items, byte scope, long startItem, long endItem, 309 @NonNull MediaController mediaController) { 310 if (DEBUG) Log.d(TAG, "getFolderItemsFilterAttr: startItem =" + startItem + ", endItem = " 311 + endItem); 312 313 List<MediaSession.QueueItem> result_items = new ArrayList<MediaSession.QueueItem>(); 314 315 if (items == null) { 316 Log.e(TAG, "items is null in getFolderItemsFilterAttr"); 317 mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null); 318 return; 319 } 320 321 result_items = checkIndexOutofBounds(bdaddr, items, startItem, endItem); 322 /* check for index out of bound errors */ 323 if (result_items == null) { 324 Log.w(TAG, "result_items is null."); 325 mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null); 326 return; 327 } 328 329 FolderItemsData folderDataNative = new FolderItemsData(result_items.size()); 330 331 /* variables to accumulate attrs */ 332 ArrayList<String> attrArray = new ArrayList<String>(); 333 ArrayList<Integer> attrId = new ArrayList<Integer>(); 334 335 for (int itemIndex = 0; itemIndex < result_items.size(); itemIndex++) { 336 MediaSession.QueueItem item = result_items.get(itemIndex); 337 // get the queue id 338 long qid = item.getQueueId(); 339 byte[] uid = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array(); 340 341 // get the array of uid from 2d to array 1D array 342 for (int idx = 0; idx < AvrcpConstants.UID_SIZE; idx++) { 343 folderDataNative.mItemUid[itemIndex * AvrcpConstants.UID_SIZE + idx] = uid[idx]; 344 } 345 346 /* Set display name for current item */ 347 folderDataNative.mDisplayNames[itemIndex] = 348 getAttrValue(AvrcpConstants.ATTRID_TITLE, item, mediaController); 349 350 int maxAttributesRequested = 0; 351 boolean isAllAttribRequested = false; 352 /* check if remote requested for attributes */ 353 if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) { 354 int attrCnt = 0; 355 356 /* add requested attr ids to a temp array */ 357 if (folderItemsReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) { 358 isAllAttribRequested = true; 359 maxAttributesRequested = AvrcpConstants.MAX_NUM_ATTR; 360 } else { 361 /* get only the requested attribute ids from the request */ 362 maxAttributesRequested = folderItemsReqObj.mNumAttr; 363 } 364 365 /* lookup and copy values of attributes for ids requested above */ 366 for (int idx = 0; idx < maxAttributesRequested; idx++) { 367 /* check if media player provided requested attributes */ 368 String value = null; 369 370 int attribId = 371 isAllAttribRequested ? (idx + 1) : folderItemsReqObj.mAttrIDs[idx]; 372 value = getAttrValue(attribId, item, mediaController); 373 if (value != null) { 374 attrArray.add(value); 375 attrId.add(attribId); 376 attrCnt++; 377 } 378 } 379 /* add num attr actually received from media player for a particular item */ 380 folderDataNative.mAttributesNum[itemIndex] = attrCnt; 381 } 382 } 383 384 /* copy filtered attr ids and attr values to response parameters */ 385 if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) { 386 folderDataNative.mAttrIds = new int[attrId.size()]; 387 for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++) 388 folderDataNative.mAttrIds[attrIndex] = attrId.get(attrIndex); 389 folderDataNative.mAttrValues = attrArray.toArray(new String[attrArray.size()]); 390 } 391 for (int attrIndex = 0; attrIndex < folderDataNative.mAttributesNum.length; attrIndex++) 392 if (DEBUG) 393 Log.d(TAG, "folderDataNative.mAttributesNum" 394 + folderDataNative.mAttributesNum[attrIndex] + " attrIndex " 395 + attrIndex); 396 397 /* create rsp object and send response to remote device */ 398 FolderItemsRsp rspObj = new FolderItemsRsp(AvrcpConstants.RSP_NO_ERROR, Avrcp.sUIDCounter, 399 scope, folderDataNative.mNumItems, folderDataNative.mFolderTypes, 400 folderDataNative.mPlayable, folderDataNative.mItemTypes, folderDataNative.mItemUid, 401 folderDataNative.mDisplayNames, folderDataNative.mAttributesNum, 402 folderDataNative.mAttrIds, folderDataNative.mAttrValues); 403 mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj); 404 } 405 406 private String getAttrValue( 407 int attr, MediaSession.QueueItem item, @Nullable MediaController mediaController) { 408 String attrValue = null; 409 if (item == null) { 410 if (DEBUG) Log.d(TAG, "getAttrValue received null item"); 411 return null; 412 } 413 try { 414 MediaDescription desc = item.getDescription(); 415 Bundle extras = desc.getExtras(); 416 if (mediaController != null) { 417 PlaybackState state = mediaController.getPlaybackState(); 418 if (state != null && (item.getQueueId() == state.getActiveQueueItemId())) { 419 if (DEBUG) Log.d(TAG, "getAttrValue: item is active, filling extra data"); 420 extras = fillBundle(mediaController.getMetadata(), extras); 421 } 422 } 423 if (DEBUG) Log.d(TAG, "getAttrValue: item " + item + " : " + desc); 424 switch (attr) { 425 case AvrcpConstants.ATTRID_TITLE: 426 /* Title is mandatory attribute */ 427 attrValue = desc.getTitle().toString(); 428 break; 429 430 case AvrcpConstants.ATTRID_ARTIST: 431 attrValue = extras.getString(MediaMetadata.METADATA_KEY_ARTIST); 432 break; 433 434 case AvrcpConstants.ATTRID_ALBUM: 435 attrValue = extras.getString(MediaMetadata.METADATA_KEY_ALBUM); 436 break; 437 438 case AvrcpConstants.ATTRID_TRACK_NUM: 439 attrValue = 440 Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER)); 441 break; 442 443 case AvrcpConstants.ATTRID_NUM_TRACKS: 444 attrValue = 445 Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS)); 446 break; 447 448 case AvrcpConstants.ATTRID_GENRE: 449 attrValue = extras.getString(MediaMetadata.METADATA_KEY_GENRE); 450 break; 451 452 case AvrcpConstants.ATTRID_PLAY_TIME: 453 attrValue = Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_DURATION)); 454 break; 455 456 case AvrcpConstants.ATTRID_COVER_ART: 457 Log.e(TAG, "getAttrValue: Cover art attribute not supported"); 458 return null; 459 460 default: 461 Log.e(TAG, "getAttrValue: Unknown attribute ID requested: " + attr); 462 return null; 463 } 464 } catch (NullPointerException ex) { 465 Log.w(TAG, "getAttrValue: attr id not found in result"); 466 /* checking if attribute is title, then it is mandatory and cannot send null */ 467 if (attr == AvrcpConstants.ATTRID_TITLE) { 468 attrValue = "<Unknown Title>"; 469 } else { 470 return null; 471 } 472 } 473 if (DEBUG) Log.d(TAG, "getAttrValue: attrvalue = " + attrValue + ", attr id:" + attr); 474 return attrValue; 475 } 476 477 private void getItemAttrFilterAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd mItemAttrReqObj, 478 MediaSession.QueueItem mediaItem, @Nullable MediaController mediaController) { 479 /* Response parameters */ 480 int[] attrIds = null; /* array of attr ids */ 481 String[] attrValues = null; /* array of attr values */ 482 int attrCounter = 0; /* num attributes for each item */ 483 /* variables to temperorily add attrs */ 484 ArrayList<String> attrArray = new ArrayList<String>(); 485 ArrayList<Integer> attrId = new ArrayList<Integer>(); 486 ArrayList<Integer> attrTempId = new ArrayList<Integer>(); 487 488 /* check if remote device has requested for attributes */ 489 if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) { 490 if (mItemAttrReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) { 491 for (int idx = 1; idx < AvrcpConstants.MAX_NUM_ATTR; idx++) { 492 attrTempId.add(idx); /* attr id 0x00 is unused */ 493 } 494 } else { 495 /* get only the requested attribute ids from the request */ 496 for (int idx = 0; idx < mItemAttrReqObj.mNumAttr; idx++) { 497 if (DEBUG) 498 Log.d(TAG, "getItemAttrFilterAttr: attr id[" + idx + "] :" 499 + mItemAttrReqObj.mAttrIDs[idx]); 500 attrTempId.add(mItemAttrReqObj.mAttrIDs[idx]); 501 } 502 } 503 } 504 505 if (DEBUG) Log.d(TAG, "getItemAttrFilterAttr: attr id list size:" + attrTempId.size()); 506 /* lookup and copy values of attributes for ids requested above */ 507 for (int idx = 0; idx < attrTempId.size(); idx++) { 508 /* check if media player provided requested attributes */ 509 String value = getAttrValue(attrTempId.get(idx), mediaItem, mediaController); 510 if (value != null) { 511 attrArray.add(value); 512 attrId.add(attrTempId.get(idx)); 513 attrCounter++; 514 } 515 } 516 517 /* copy filtered attr ids and attr values to response parameters */ 518 if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) { 519 attrIds = new int[attrId.size()]; 520 521 for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++) 522 attrIds[attrIndex] = attrId.get(attrIndex); 523 524 attrValues = attrArray.toArray(new String[attrId.size()]); 525 526 /* create rsp object and send response */ 527 ItemAttrRsp rspObj = new ItemAttrRsp(AvrcpConstants.RSP_NO_ERROR, 528 (byte)attrCounter, attrIds, attrValues); 529 mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj); 530 return; 531 } 532 } 533} 534