Skip to content

Commit

Permalink
Transcript navigation
Browse files Browse the repository at this point in the history
Display of video transcripts with active sentence highlighting. Tapping on transcript sentences to navigate to the corresponding part of the video. Mechanism to quickly return to the current active sentence after scrolling.

https://github.com/openedx/openedx-app-ios/assets/128456094/b2cf1f49-e23b-42dd-a9b0-0ddac26bda59
  • Loading branch information
Stepanokdev committed Oct 6, 2023
1 parent d029734 commit 3abc7ce
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 15 deletions.
13 changes: 13 additions & 0 deletions Core/Core/Extensions/DateExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ public enum DateStringStyle {
}

public extension Date {

func secondsSinceMidnight() -> Double {
let calendar = Calendar.current
let components = calendar.dateComponents([.hour, .minute, .second], from: self)

guard let hours = components.hour, let minutes = components.minute, let seconds = components.second else {
return 0.0
}

let totalSeconds = Double(hours) * 3600.0 + Double(minutes) * 60.0 + Double(seconds)
return totalSeconds
}

func dateToString(style: DateStringStyle) -> String {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
Expand Down
20 changes: 17 additions & 3 deletions Course/Course/Presentation/Video/EncodedVideoPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public struct EncodedVideoPlayer: View {
@State private var isViewedOnce: Bool = false
@State private var currentTime: Double = 0
@State private var isOrientationChanged: Bool = false
@State private var pause: Bool = false

@State var showAlert = false
@State var alertMessage: String? {
Expand Down Expand Up @@ -64,7 +65,9 @@ public struct EncodedVideoPlayer: View {
}
}
}, seconds: { seconds in
currentTime = seconds
if !pause {
currentTime = seconds
}
})
.aspectRatio(16 / 9, contentMode: .fit)
.cornerRadius(12)
Expand All @@ -89,8 +92,13 @@ public struct EncodedVideoPlayer: View {
}
}
SubtittlesView(languages: viewModel.languages,
currentTime: $currentTime,
viewModel: viewModel)
currentTime: $currentTime,
viewModel: viewModel, scrollTo: { date in
viewModel.controller.player?.seek(to: CMTime(seconds: date.secondsSinceMidnight(),
preferredTimescale: 10000))
pauseScrolling()
currentTime = (date.secondsSinceMidnight() + 1)
})
Spacer()
if !orientation.isLandscape || idiom != .pad {
VStack {}.onAppear {
Expand Down Expand Up @@ -120,6 +128,12 @@ public struct EncodedVideoPlayer: View {
}
}
}
private func pauseScrolling() {
pause = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.pause = false
}
}
}

#if DEBUG
Expand Down
36 changes: 27 additions & 9 deletions Course/Course/Presentation/Video/SubtittlesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,21 @@ public struct SubtittlesView: View {

@ObservedObject
private var viewModel: VideoPlayerViewModel
private var scrollTo: ((Date) -> Void) = { _ in }

@Binding var currentTime: Double
@State var id = 0
@State var pause: Bool = false
@State var languages: [SubtitleUrl]

public init(languages: [SubtitleUrl],
currentTime: Binding<Double>,
viewModel: VideoPlayerViewModel) {
viewModel: VideoPlayerViewModel,
scrollTo: @escaping (Date) -> Void) {
self.languages = languages
self.viewModel = viewModel
self._currentTime = currentTime
self.scrollTo = scrollTo
}

public var body: some View {
Expand All @@ -51,42 +55,56 @@ public struct SubtittlesView: View {
}
}
ZStack {

ScrollView {
if viewModel.subtitles.count > 0 {
VStack(alignment: .leading, spacing: 0) {
ForEach(viewModel.subtitles, id: \.id) { subtitle in
HStack {
Button(action: {
scrollTo(subtitle.fromTo.start)
}, label: {
Text(subtitle.text)
.padding(.vertical, 16)
.font(Theme.Fonts.bodyMedium)
.multilineTextAlignment(.leading)
.foregroundColor(subtitle.fromTo.contains(Date(milliseconds: currentTime))
? Theme.Colors.textPrimary
: Theme.Colors.textSecondary)

.onChange(of: currentTime, perform: { _ in
if subtitle.fromTo.contains(Date(milliseconds: currentTime)) {
if id != subtitle.id {
withAnimation {
scroll.scrollTo(subtitle.id, anchor: .top)
if !pause {
scroll.scrollTo(subtitle.id, anchor: .top)
}
}
}
self.id = subtitle.id
}
})
})
}.id(subtitle.id)
}
}
.introspect(.scrollView, on: .iOS(.v14, .v15, .v16, .v17), customize: { scrollView in
scrollView.isScrollEnabled = false
})
}
}
// Forced disable scrolling for iOS 14, 15
Color.white.opacity(0)
}.simultaneousGesture(
DragGesture().onChanged({ _ in
pauseScrolling()
}))
}
}.padding(.horizontal, 24)
.padding(.top, 34)
}
}

private func pauseScrolling() {
pause = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.pause = false
}
}
}

#if DEBUG
Expand All @@ -103,7 +121,7 @@ struct SubtittlesView_Previews: PreviewProvider {
interactor: CourseInteractor(repository: CourseRepositoryMock()),
router: CourseRouterMock(),
connectivity: Connectivity()
)
), scrollTo: {_ in }
)
}
}
Expand Down
7 changes: 5 additions & 2 deletions Course/Course/Presentation/Video/YouTubeVideoPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ public struct YouTubeVideoPlayer: View {
@StateObject
private var viewModel: YouTubeVideoPlayerViewModel
private var isOnScreen: Bool

@State
private var showAlert = false
@State
Expand Down Expand Up @@ -69,7 +68,11 @@ public struct YouTubeVideoPlayer: View {
SubtittlesView(
languages: viewModel.languages,
currentTime: $viewModel.currentTime,
viewModel: viewModel
viewModel: viewModel, scrollTo: { date in
viewModel.youtubePlayer.seek(to: date.secondsSinceMidnight(), allowSeekAhead: true)
viewModel.pauseScrolling()
viewModel.currentTime = date.secondsSinceMidnight() + 1
}
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel {
private (set) var play = false
@Published var isLoading: Bool = true
@Published var currentTime: Double = 0
@Published var pause: Bool = false

private var subscription = Set<AnyCancellable>()
private var duration: Double?
Expand Down Expand Up @@ -68,6 +69,13 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel {
subscrube(playerStateSubject: playerStateSubject)
}

func pauseScrolling() {
pause = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.pause = false
}
}

private func subscrube(playerStateSubject: CurrentValueSubject<VideoPlayerState?, Never>) {
playerStateSubject.sink(receiveValue: { [weak self] state in
switch state {
Expand All @@ -84,7 +92,9 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel {

youtubePlayer.currentTimePublisher(updateInterval: 0.1).sink(receiveValue: { [weak self] time in
guard let self else { return }
self.currentTime = time
if !self.pause {
self.currentTime = time
}

if let duration = self.duration {
if (time / duration) >= 0.8 {
Expand Down

0 comments on commit 3abc7ce

Please sign in to comment.