private VideoPlayer.RendererBuilder getRendererBuilder() { String userAgent = Util.getUserAgent(getActivity(), "ExoVideoPlayer"); Uri contentUri = Uri.parse(mSelectedVideo.videoUrl); int contentType = Util.inferContentType(contentUri.getLastPathSegment()); switch (contentType) { case Util.TYPE_OTHER: { return new ExtractorRendererBuilder(getActivity(), userAgent, contentUri); } case Util.TYPE_DASH: { // Implement your own DRM callback here. MediaDrmCallback drmCallback = new WidevineTestMediaDrmCallback(null, null); return new DashRendererBuilder(getActivity(), userAgent, contentUri.toString(), drmCallback); } case Util.TYPE_HLS: { return new HlsRendererBuilder(getActivity(), userAgent, contentUri.toString()); } default: { throw new IllegalStateException("Unsupported type: " + contentType); } } }
private RendererBuilder getRendererBuilder() { Uri contentUri = Uri.parse(mDataSource); String userAgent = Util.getUserAgent(mAppContext, "IjkExoMediaPlayer"); int contentType = inferContentType(contentUri); switch (contentType) { case Util.TYPE_SS: return new SmoothStreamingRendererBuilder(mAppContext, userAgent, contentUri.toString(), new SmoothStreamingTestMediaDrmCallback()); /* case Util.TYPE_DASH: return new DashRendererBuilder(mAppContext , userAgent, contentUri.toString(), new WidevineTestMediaDrmCallback(contentId, provider));*/ case Util.TYPE_HLS: return new HlsRendererBuilder(mAppContext, userAgent, contentUri.toString()); case Util.TYPE_OTHER: default: return new ExtractorRendererBuilder(mAppContext, userAgent, contentUri); } }
public StreamElement(String baseUri, String chunkTemplate, int type, String subType, long timescale, String name, int qualityLevels, int maxWidth, int maxHeight, int displayWidth, int displayHeight, String language, TrackElement[] tracks, List<Long> chunkStartTimes, long lastChunkDuration) { this.baseUri = baseUri; this.chunkTemplate = chunkTemplate; this.type = type; this.subType = subType; this.timescale = timescale; this.name = name; this.qualityLevels = qualityLevels; this.maxWidth = maxWidth; this.maxHeight = maxHeight; this.displayWidth = displayWidth; this.displayHeight = displayHeight; this.language = language; this.tracks = tracks; this.chunkCount = chunkStartTimes.size(); this.chunkStartTimes = chunkStartTimes; lastChunkDurationUs = Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale); chunkStartTimesUs = Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale); }
/** * @param sources The upstream sources from which the renderer obtains samples. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted media. May be null if support for encrypted * media is not required. * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. * For example a media file may start with a short clear region so as to allow playback to * begin in parallel with key acquisition. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ public MediaCodecTrackRenderer(SampleSource[] sources, MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) { super(sources); Assertions.checkState(Util.SDK_INT >= 16); this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector); this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; this.eventHandler = eventHandler; this.eventListener = eventListener; deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround(); codecCounters = new CodecCounters(); sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); formatHolder = new MediaFormatHolder(); decodeOnlyPresentationTimestamps = new ArrayList<>(); outputBufferInfo = new MediaCodec.BufferInfo(); codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReinitializationState = REINITIALIZATION_STATE_NONE; }
@SuppressWarnings("NonAtomicVolatileUpdate") @Override public void load() throws IOException, InterruptedException { DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); try { // Create and open the input. ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); if (bytesLoaded == 0) { // Set the target to ourselves. extractorWrapper.init(this); } // Load and parse the initialization data. try { int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { result = extractorWrapper.read(input); } } finally { bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } } finally { dataSource.close(); } }
private RendererBuilder getRendererBuilder() { String userAgent = Util.getUserAgent(getActivity(), "ExoPlayerDemo"); Uri uri = Uri.parse(mStreamUrl); int contentType = inferContentType(uri, null); switch (contentType) { // case TYPE_SS: // return new SmoothStreamingRendererBuilder(this, null, contentUri.toString(), // new SmoothStreamingTestMediaDrmCallback()); // case TYPE_DASH: // return new DashRendererBuilder(this, userAgent, contentUri.toString(), // new WidevineTestMediaDrmCallback(contentId, provider)); case TYPE_HLS: return new HlsRendererBuilder(getContext(), userAgent, mStreamUrl); case TYPE_OTHER: return new ExtractorRendererBuilder(getContext(), userAgent, uri); default: throw new IllegalStateException("Unsupported type: " + contentType); } }
/** * Given viewport dimensions and video dimensions, computes the maximum size of the video as it * will be rendered to fit inside of the viewport. */ private static Point getMaxVideoSizeInViewport(boolean orientationMayChange, int viewportWidth, int viewportHeight, int videoWidth, int videoHeight) { if (orientationMayChange && (videoWidth > videoHeight) != (viewportWidth > viewportHeight)) { // Rotation is allowed, and the video will be larger in the rotated viewport. int tempViewportWidth = viewportWidth; viewportWidth = viewportHeight; viewportHeight = tempViewportWidth; } if (videoWidth * viewportHeight >= videoHeight * viewportWidth) { // Horizontal letter-boxing along top and bottom. return new Point(viewportWidth, Util.ceilDivide(viewportWidth * videoHeight, videoWidth)); } else { // Vertical letter-boxing along edges. return new Point(Util.ceilDivide(viewportHeight * videoWidth, videoHeight), viewportHeight); } }
/** * @param source The upstream source from which the renderer obtains samples. * @param drmSessionManager For use with encrypted media. May be null if support for encrypted * media is not required. * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. * For example a media file may start with a short clear region so as to allow playback * to * begin in parallel with key acquisision. This parameter specifies whether the renderer * is * permitted to play clear regions of encrypted media files before * {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ public MediaCodecTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) { Assertions.checkState(Util.SDK_INT >= 16); this.source = source.register(); this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; this.eventHandler = eventHandler; this.eventListener = eventListener; codecCounters = new CodecCounters(); sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); formatHolder = new MediaFormatHolder(); decodeOnlyPresentationTimestamps = new ArrayList<>(); outputBufferInfo = new MediaCodec.BufferInfo(); codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReinitializationState = REINITIALIZATION_STATE_NONE; }
private void flushCodec() throws ExoPlaybackException { codecHotswapTimeMs = -1; inputIndex = -1; outputIndex = -1; waitingForFirstSyncFrame = true; waitingForKeys = false; decodeOnlyPresentationTimestamps.clear(); // Workaround for framework bugs. // See [Internal: b/8347958], [Internal: b/8578467], [Internal: b/8543366]. if (Util.SDK_INT >= 18 && codecReinitializationState == REINITIALIZATION_STATE_NONE) { codec.flush(); codecHasQueuedBuffers = false; } else { releaseCodec(); maybeInitCodec(); } if (codecReconfigured && format != null) { // Any reconfiguration data that we send shortly before the flush may be discarded. We // avoid this issue by sending reconfiguration data following every flush. codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; } }
public AudioTrack() { releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { try { getLatencyMethod = android.media.AudioTrack.class.getMethod("getLatency", (Class<?>[]) null); } catch (NoSuchMethodException e) { // There's no guarantee this method exists. Do nothing. } } if (Util.SDK_INT >= 19) { audioTrackUtil = new AudioTrackUtilV19(); } else { audioTrackUtil = new AudioTrackUtil(); } playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; }
/** * {@link android.media.AudioTrack#getPlaybackHeadPosition()} returns a value intended to be * interpreted as an unsigned 32 bit integer, which also wraps around periodically. This method * returns the playback head position as a long that will only wrap around if the value exceeds * {@link Long#MAX_VALUE} (which in practice will never happen). * * @return {@link android.media.AudioTrack#getPlaybackHeadPosition()} of {@link #audioTrack} * expressed as a long. */ public long getPlaybackHeadPosition() { long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); if (Util.SDK_INT <= 22 && isPassthrough) { // Work around issues with passthrough/direct AudioTracks on platform API versions 21/22: // - After resetting, the new AudioTrack's playback position continues to increase for a // short time from the old AudioTrack's position, while in the PLAYSTATE_STOPPED state. // - The playback head position jumps back to zero on paused passthrough/direct audio // tracks. See [Internal: b/19187573]. if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_STOPPED) { // Prevent detecting a wrapped position. lastRawPlaybackHeadPosition = rawPlaybackHeadPosition; } else if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) { passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition; } rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; } if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) { // The value must have wrapped around. rawPlaybackHeadWrapCount++; } lastRawPlaybackHeadPosition = rawPlaybackHeadPosition; return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32); }
/** * Returns whether the specified codec is usable for decoding on the current device. */ private static boolean isCodecUsableDecoder(MediaCodecInfo info, String name, boolean secureDecodersExplicit) { if (info.isEncoder() || !name.startsWith("OMX.") || (!secureDecodersExplicit && name.endsWith(".secure"))) { return false; } // Work around an issue where creating a particular MP3 decoder on some devices on platform API // version 16 crashes mediaserver. if (Util.SDK_INT == 16 && ("dlxu".equals(Util.DEVICE) // HTC Butterfly || "protou".equals(Util.DEVICE) // HTC Desire X || "C6602".equals(Util.DEVICE) || "C6603".equals(Util.DEVICE)) // Sony Xperia Z && name.equals("OMX.qcom.audio.decoder.mp3")) { return false; } // Work around an issue where the VP8 decoder on Samsung Galaxy S4 Mini does not render video. if (Util.SDK_INT <= 19 && Util.DEVICE != null && Util.DEVICE.startsWith("serrano") && "samsung".equals(Util.MANUFACTURER) && name.equals("OMX.SEC.vp8.dec")) { return false; } return true; }
@Override public void close() throws HttpDataSourceException { try { if (inputStream != null) { Util.maybeTerminateInputStream(connection, bytesRemaining()); try { inputStream.close(); } catch (IOException e) { throw new HttpDataSourceException(e, dataSpec); } } } finally { inputStream = null; closeConnection(); if (opened) { opened = false; if (listener != null) { listener.onTransferEnd(); } } } }
private void closeCurrentOutputStream() throws IOException { if (outputStream == null) { return; } boolean success = false; try { outputStream.flush(); outputStream.getFD().sync(); success = true; } finally { Util.closeQuietly(outputStream); if (success) { cache.commitFile(file); } else { file.delete(); } outputStream = null; file = null; } }
@Override public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { String url = request.getDefaultUrl(); if (TextUtils.isEmpty(url)) { url = PLAYREADY_TEST_DEFAULT_URI; } return Util.executePost(url, request.getData(), KEY_REQUEST_PROPERTIES); }
public SmoothStreamingRendererBuilder(Context context, String userAgent, String url, MediaDrmCallback drmCallback) { this.context = context; this.userAgent = userAgent; this.url = Util.toLowerInvariant(url).endsWith("/manifest") ? url : url + "/Manifest"; this.drmCallback = drmCallback; }
@Override public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws IOException { String url = request.getDefaultUrl(); if (TextUtils.isEmpty(url)) { url = defaultUri; } return Util.executePost(url, request.getData(), null); }
private VideoPlayer.RendererBuilder getRendererBuilder() { String userAgent = Util.getUserAgent(getActivity(), "ExoVideoPlayer"); Uri contentUri = Uri.parse(mSelectedVideo.videoUrl); int contentType = Util.inferContentType(contentUri.getLastPathSegment()); switch (contentType) { case Util.TYPE_OTHER: { return new ExtractorRendererBuilder(getActivity(), userAgent, contentUri); } default: { throw new IllegalStateException("Unsupported type: " + contentType); } } }
private RendererBuilder getHpLibRendererBuilder() { String userAgent = Util.getUserAgent(activity, "HpLib"); switch (video_type.get(currentTrackIndex)) { case "hls": return new HlsRendererBuilder(activity, userAgent, video_url.get(currentTrackIndex)); case "others": return new ExtractorRendererBuilder(activity, userAgent, Uri.parse(video_url.get(currentTrackIndex))); default: throw new IllegalStateException("Unsupported type: " + video_url.get(currentTrackIndex)); } }
public static boolean isCrappySamsung() { if (Util.SDK_INT <= 19 && "samsung".equals(Util.MANUFACTURER)) { return true; } return false; }
@Override public Long parse(String connectionUrl, InputStream inputStream) throws ParserException, IOException { String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); try { return Util.parseXsDateTime(firstLine); } catch (ParseException e) { throw new ParserException(e); } }
public void test24FpsH264Fixed() throws IOException { if (Util.SDK_INT < 23) { // Pass. return; } String streamName = "test_24fps_h264_fixed"; testDashPlayback(getActivity(), streamName, H264_24_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, false, H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID); }
@Override public void onError(Exception e) { if (e instanceof UnsupportedDrmException) { // Special case DRM failures. UnsupportedDrmException unsupportedDrmException = (UnsupportedDrmException) e; int stringId = Util.SDK_INT < 18 ? R.string.drm_error_not_supported : unsupportedDrmException.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME ? R.string.drm_error_unsupported_scheme : R.string.drm_error_unknown; Toast.makeText(getApplicationContext(), stringId, Toast.LENGTH_LONG).show(); } playerNeedsPrepare = true; updateButtonVisibilities(); showControls(); goatMediaController.showError(); }
/** * Instantiates a new sample extractor reading from the specified {@code uri}. * * @param context Context for resolving {@code uri}. * @param uri The content URI from which to extract data. * @param headers Headers to send with requests for data. */ public FrameworkSampleSource(Context context, Uri uri, Map<String, String> headers) { Assertions.checkState(Util.SDK_INT >= 16); this.context = Assertions.checkNotNull(context); this.uri = Assertions.checkNotNull(uri); this.headers = headers; fileDescriptor = null; fileDescriptorOffset = 0; fileDescriptorLength = 0; }
@Override public void onError(Exception e) { String errorString = null; if (e instanceof UnsupportedDrmException) { // Special case DRM failures. UnsupportedDrmException unsupportedDrmException = (UnsupportedDrmException) e; errorString = getString(Util.SDK_INT < 18 ? R.string.video_error_drm_not_supported : unsupportedDrmException.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME ? R.string.video_error_drm_unsupported_scheme : R.string.video_error_drm_unknown); } else if (e instanceof ExoPlaybackException && e.getCause() instanceof DecoderInitializationException) { // Special case for decoder initialization failures. DecoderInitializationException decoderInitializationException = (DecoderInitializationException) e.getCause(); if (decoderInitializationException.decoderName == null) { if (decoderInitializationException.getCause() instanceof DecoderQueryException) { errorString = getString(R.string.video_error_querying_decoders); } else if (decoderInitializationException.secureDecoderRequired) { errorString = getString(R.string.video_error_no_secure_decoder, decoderInitializationException.mimeType); } else { errorString = getString(R.string.video_error_no_decoder, decoderInitializationException.mimeType); } } else { errorString = getString(R.string.video_error_instantiating_decoder, decoderInitializationException.decoderName); } } if (errorString != null) { Toast.makeText(getApplicationContext(), errorString, Toast.LENGTH_LONG).show(); } playerNeedsPrepare = true; showControls(); }
/** * Returns the sample index of the closest synchronization sample at or before the given * timestamp, if one is available. * * @param timeUs Timestamp adjacent to which to find a synchronization sample. * @return Index of the synchronization sample, or {@link #NO_SAMPLE} if none. */ public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) { // Video frame timestamps may not be sorted, so the behavior of this call can be undefined. // Frames are not reordered past synchronization samples so this works in practice. int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false); for (int i = startIndex; i >= 0; i--) { if ((flags[i] & C.SAMPLE_FLAG_SYNC) != 0) { return i; } } return NO_SAMPLE; }
private void configureSubtitleView() { CaptionStyleCompat style; float fontScale; if (Util.SDK_INT >= 19) { style = getUserCaptionStyleV19(); fontScale = getUserCaptionFontScaleV19(); } else { style = CaptionStyleCompat.DEFAULT; fontScale = 1.0f; } subtitleLayout.setStyle(style); subtitleLayout.setFractionalTextSize(SubtitleLayout.DEFAULT_TEXT_SIZE_FRACTION * fontScale); }
@Override protected void onOutputFormatChanged(android.media.MediaFormat outputFormat) { boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT) && outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM) && outputFormat.containsKey(KEY_CROP_TOP); currentWidth = hasCrop ? outputFormat.getInteger(KEY_CROP_RIGHT) - outputFormat.getInteger(KEY_CROP_LEFT) + 1 : outputFormat.getInteger(android.media.MediaFormat.KEY_WIDTH); currentHeight = hasCrop ? outputFormat.getInteger(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1 : outputFormat.getInteger(android.media.MediaFormat.KEY_HEIGHT); currentPixelWidthHeightRatio = pendingPixelWidthHeightRatio; if (Util.SDK_INT >= 21) { // On API level 21 and above the decoder applies the rotation when rendering to the surface. // Hence currentUnappliedRotation should always be 0. For 90 and 270 degree rotations, we need // to flip the width, height and pixel aspect ratio to reflect the rotation that was applied. if (pendingRotationDegrees == 90 || pendingRotationDegrees == 270) { int rotatedHeight = currentWidth; currentWidth = currentHeight; currentHeight = rotatedHeight; currentPixelWidthHeightRatio = 1 / currentPixelWidthHeightRatio; } } else { // On API level 20 and below the decoder does not apply the rotation. currentUnappliedRotationDegrees = pendingRotationDegrees; } }
public void testH264Adaptive() throws IOException { if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { // Pass. return; } String streamName = "test_h264_adaptive"; testDashPlayback(getActivity(), streamName, H264_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, ALLOW_ADDITIONAL_VIDEO_FORMATS, H264_CDD_ADAPTIVE); }
public DecoderInitializationException(MediaFormat mediaFormat, Throwable cause, boolean secureDecoderRequired, String decoderName) { super("Decoder init failed: " + decoderName + ", " + mediaFormat, cause); this.mimeType = mediaFormat.mimeType; this.secureDecoderRequired = secureDecoderRequired; this.decoderName = decoderName; this.diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null; }
/** * Instantiates a new sample extractor reading from the specified seekable {@code fileDescriptor}. * The caller is responsible for releasing the file descriptor. * * @param fileDescriptor File descriptor from which to read. * @param fileDescriptorOffset The offset in bytes where the data to be extracted starts. * @param fileDescriptorLength The length in bytes of the data to be extracted. */ public FrameworkSampleSource(FileDescriptor fileDescriptor, long fileDescriptorOffset, long fileDescriptorLength) { Assertions.checkState(Util.SDK_INT >= 16); this.fileDescriptor = Assertions.checkNotNull(fileDescriptor); this.fileDescriptorOffset = fileDescriptorOffset; this.fileDescriptorLength = fileDescriptorLength; context = null; uri = null; headers = null; }
@Override public int readData(int track, long positionUs, MediaFormatHolder formatHolder, SampleHolder sampleHolder, boolean onlyReadDiscontinuity) { Assertions.checkState(prepared); Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); if (pendingDiscontinuities[track]) { pendingDiscontinuities[track] = false; return DISCONTINUITY_READ; } if (onlyReadDiscontinuity) { return NOTHING_READ; } if (trackStates[track] != TRACK_STATE_FORMAT_SENT) { formatHolder.format = MediaFormat.createFromFrameworkMediaFormatV16( extractor.getTrackFormat(track)); formatHolder.drmInitData = Util.SDK_INT >= 18 ? getDrmInitDataV18() : null; trackStates[track] = TRACK_STATE_FORMAT_SENT; return FORMAT_READ; } int extractorTrackIndex = extractor.getSampleTrackIndex(); if (extractorTrackIndex == track) { if (sampleHolder.data != null) { int offset = sampleHolder.data.position(); sampleHolder.size = extractor.readSampleData(sampleHolder.data, offset); sampleHolder.data.position(offset + sampleHolder.size); } else { sampleHolder.size = 0; } sampleHolder.timeUs = extractor.getSampleTime(); sampleHolder.flags = extractor.getSampleFlags() & ALLOWED_FLAGS_MASK; if (sampleHolder.isEncrypted()) { sampleHolder.cryptoInfo.setFromExtractorV16(extractor); } seekPositionUs = C.UNKNOWN_TIME_US; extractor.advance(); return SAMPLE_READ; } else { return extractorTrackIndex < 0 ? END_OF_STREAM : NOTHING_READ; } }