diff --git a/packages/video_player_videohole/CHANGELOG.md b/packages/video_player_videohole/CHANGELOG.md index c01452c7f..3245e0e23 100644 --- a/packages/video_player_videohole/CHANGELOG.md +++ b/packages/video_player_videohole/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.5.10 + +* Fix crash/seek failures on progressive HTTP MPEG-TS live streams. +* Avoid treating all `.ts` URLs as live so finite MPEG-TS VOD stays seekable. + ## 0.5.9 * Adds compatibility with `http` 1.0 in example. diff --git a/packages/video_player_videohole/lib/video_player.dart b/packages/video_player_videohole/lib/video_player.dart index 9377a9b1f..54a80a252 100644 --- a/packages/video_player_videohole/lib/video_player.dart +++ b/packages/video_player_videohole/lib/video_player.dart @@ -469,19 +469,23 @@ class VideoPlayerController extends ValueNotifier { if (VideoEventType.restored == event.eventType && _onRestoreDataSource != null) { play(); - } else { + } else if (value.isPlaying) { + // Avoid pause before play; live/init can report Pause failed. _applyPlayPause(); } _durationTimer?.cancel(); _durationTimer = _createDurationTimer(); case VideoEventType.completed: - // In this case we need to stop _timer, set isPlaying=false, and - // position=value.duration. Instead of setting the values directly, - // we use pause() and seekTo() to ensure the platform stops playing - // and seeks to the last frame of the video. - pause().then((void pauseResult) => seekTo(value.duration.end)); - value = value.copyWith(isCompleted: true); - _durationTimer?.cancel(); + // Live / placeholder duration: skip pause+seek (plain .ts fails). + if (_looksLikeLiveOrOpenEnded) { + value = value.copyWith(isPlaying: false, isCompleted: true); + _timer?.cancel(); + _durationTimer?.cancel(); + } else { + pause().then((void pauseResult) => seekTo(value.duration.end)); + value = value.copyWith(isCompleted: true); + _durationTimer?.cancel(); + } case VideoEventType.bufferingUpdate: value = value.copyWith(buffered: event.buffered); case VideoEventType.bufferingStart: @@ -573,13 +577,31 @@ class VideoPlayerController extends ValueNotifier { /// has been sent to the platform, not when playback itself is totally /// finished. Future play() async { - if (value.position == value.duration.end) { + // Do not seekTo(0) when duration is ~0 or at end; live MPEG-TS is + // often not seekable. + final Duration end = value.duration.end; + if (!_looksLikeLiveOrOpenEnded && + end > Duration.zero && + value.position >= end) { await seekTo(Duration.zero); } value = value.copyWith(isPlaying: true); await _applyPlayPause(); } + /// Live / open-ended heuristic: tiny/placeholder duration, or `/live/` in URI. + /// + /// Do not treat every `.ts` URL as live — finite MPEG-TS VOD must remain + /// seekable. Progressive live `.ts` typically reports a ~0/1ms duration from + /// the platform and is covered by the duration check. + bool get _looksLikeLiveOrOpenEnded { + final Duration end = value.duration.end; + if (end <= const Duration(milliseconds: 1)) { + return true; + } + return dataSource.toLowerCase().contains('/live/'); + } + /// Sets the video activated. Use it if create two native players. Future activate() async { return _applyActivate(); @@ -721,6 +743,9 @@ class VideoPlayerController extends ValueNotifier { if (_isDisposedOrNotInitialized) { return; } + if (_looksLikeLiveOrOpenEnded) { + return; + } if (position > value.duration.end) { position = value.duration.end; } else if (position < Duration.zero) { diff --git a/packages/video_player_videohole/pubspec.yaml b/packages/video_player_videohole/pubspec.yaml index fefb4b06e..75a843b77 100644 --- a/packages/video_player_videohole/pubspec.yaml +++ b/packages/video_player_videohole/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_videohole description: Flutter plugin for displaying inline video on Tizen TV devices. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/video_player_videohole -version: 0.5.9 +version: 0.5.10 environment: sdk: ">=3.1.0 <4.0.0" diff --git a/packages/video_player_videohole/tizen/src/media_player.cc b/packages/video_player_videohole/tizen/src/media_player.cc index 1cfefa0c1..d52a0d962 100644 --- a/packages/video_player_videohole/tizen/src/media_player.cc +++ b/packages/video_player_videohole/tizen/src/media_player.cc @@ -6,6 +6,8 @@ #include +#include +#include #include #include "log.h" @@ -231,13 +233,13 @@ bool MediaPlayer::Pause() { if (ret != PLAYER_ERROR_NONE) { LOG_ERROR("[MediaPlayer] Unable to get player state."); } - if (state == PLAYER_STATE_NONE || state == PLAYER_STATE_IDLE) { - LOG_ERROR("[MediaPlayer] Player not ready."); - return false; - } - if (state != PLAYER_STATE_PLAYING) { - LOG_INFO("[MediaPlayer] Player not playing."); - return false; + // Treat pause as success when not PLAYING (e.g. live init) so Dart + // does not see a PlatformException. + if (state == PLAYER_STATE_NONE || state == PLAYER_STATE_IDLE || + state != PLAYER_STATE_PLAYING) { + LOG_INFO("[MediaPlayer] Player not playing; treat pause as no-op success."); + SendIsPlayingState(false); + return true; } ret = player_pause(player_); if (ret != PLAYER_ERROR_NONE) { @@ -287,6 +289,24 @@ bool MediaPlayer::SetPlaybackSpeed(double speed) { bool MediaPlayer::SeekTo(int64_t position, SeekCompletedCallback callback) { LOG_INFO("[MediaPlayer] position: %lld.", position); + // Live / open-ended streams are typically not seekable. + if (IsAdaptiveLive() || IsProgressiveLiveUri()) { + LOG_INFO("[MediaPlayer] Skip seek on live stream."); + if (callback) { + callback(); + } + return true; + } + int duration_ms = 0; + int duration_ret = player_get_duration(player_, &duration_ms); + if (duration_ret == PLAYER_ERROR_NONE && duration_ms <= 0) { + LOG_INFO("[MediaPlayer] Skip seek on open-ended stream (duration=0)."); + if (callback) { + callback(); + } + return true; + } + on_seek_completed_ = std::move(callback); int ret = player_set_play_position(player_, position, true, OnSeekCompleted, this); @@ -311,18 +331,30 @@ int64_t MediaPlayer::GetPosition() { } std::pair MediaPlayer::GetDuration() { - if (IsLive()) { - return GetLiveDuration(); - } else { - int duration = 0; - int ret = player_get_duration(player_, &duration); - if (ret != PLAYER_ERROR_NONE) { - LOG_ERROR("[MediaPlayer] player_get_duration failed: %s.", - get_error_message(ret)); + // Only adaptive live may use GetLiveDuration. Calling that API on + // progressive MPEG-TS can SIGSEGV. + if (IsAdaptiveLive()) { + std::pair live = GetLiveDuration(); + if (live.second <= 0) { + LOG_INFO("[MediaPlayer] Live duration unavailable; use placeholder 1ms."); + return std::make_pair(0, 1); } - LOG_INFO("[MediaPlayer] Video duration: %d.", duration); - return std::make_pair(0, duration); + return live; + } + + int duration = 0; + int ret = player_get_duration(player_, &duration); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] player_get_duration failed: %s.", + get_error_message(ret)); } + LOG_INFO("[MediaPlayer] Video duration: %d.", duration); + // Open-ended / progressive live often reports 0; use 1ms so Dart does not + // treat the stream as ended and call seekTo(0). + if (duration <= 0) { + duration = 1; + } + return std::make_pair(0, duration); } void MediaPlayer::GetVideoSize(int32_t *width, int32_t *height) { @@ -391,7 +423,18 @@ bool MediaPlayer::SetDisplay() { return true; } -bool MediaPlayer::IsLive() { +bool MediaPlayer::IsProgressiveLiveUri() const { + // Path hint only — do not treat every ".ts" URL as live (VOD MPEG-TS + // must remain seekable). Progressive live without "/live/" is handled via + // duration==0 in GetDuration/SeekTo. + std::string lower = url_; + std::transform( + lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return lower.find("/live/") != std::string::npos; +} + +bool MediaPlayer::IsAdaptiveLive() { int is_live = 0; int ret = media_player_proxy_->player_get_adaptive_streaming_info( player_, &is_live, PLAYER_ADAPTIVE_INFO_IS_LIVE); @@ -403,6 +446,10 @@ bool MediaPlayer::IsLive() { return is_live != 0; } +bool MediaPlayer::IsLive() { + return IsProgressiveLiveUri() || IsAdaptiveLive(); +} + static std::vector split(const std::string &s, char delim) { std::stringstream ss(s); std::string item; diff --git a/packages/video_player_videohole/tizen/src/media_player.h b/packages/video_player_videohole/tizen/src/media_player.h index 3a3506c2d..1e4bc5c6f 100644 --- a/packages/video_player_videohole/tizen/src/media_player.h +++ b/packages/video_player_videohole/tizen/src/media_player.h @@ -50,6 +50,8 @@ class MediaPlayer : public VideoPlayer { private: std::pair GetLiveDuration(); bool IsLive(); + bool IsAdaptiveLive(); + bool IsProgressiveLiveUri() const; bool SetDisplay(); bool SetDrm(const std::string &uri, int drm_type, const std::string &license_server_url);