I'm building an app that basically is a YouTube clone. I use the official video_player plugin for playback and chewie for controls. I'd like to implement a quality switcher, so the user can decide what quality they want the video to be streamed at
I've built a bottom sheet with switches and I run changeQuality() when the user selects the desired quality. What it should do is simply giving a new source file to the old player and keep playing from where the video left.
This is the video player and chewie player that run on initState():
videoPlayer = VideoPlayerController.network(data == null
? dataAll[indexNo]["video"]["480"]
: data[indexNo]["video"]["480"]);
chewieController = ChewieController(
videoPlayerController: videoPlayer,
aspectRatio: 16 / 9,
autoPlay: true,
allowedScreenSleep: false,
placeholder: data == null
? Image(
image: NetworkImage(dataAll[indexNo]["thumbnail"]),
)
: Image(
image: NetworkImage(data[indexNo]["thumbnail"]),
)
);
And the changeQuality() function:
changeQuality(String newQuality) {
setState(() {
position = videoPlayer.value.position;
chewieController.pause();
videoPlayer = new VideoPlayerController.network(data == null
? dataAll[indexNo]["video"]["$newQuality"]
: data[indexNo]["video"]["$newQuality"]);
chewieController = ChewieController(
videoPlayerController: videoPlayer,
aspectRatio: 16 / 9,
autoPlay: true,
allowedScreenSleep: false,
startAt: position,
);
});
Navigator.of(context).pop();
}
I've also tried disposing the old video player and then setting the new value, but I get an error that variables cannot be used after being disposed.
The switcher works a bit, because it changes the quality around 4 to 5 times and then it runs into an error and won't play anything.
I expand upon this solution for video_player and extend it to also cover chewie.
Key parts of this solution
You need two widgets. MyVideoPlayer that encapsulates video_player and chewie and an outer widget where you react to user input or state changes and swap out MyVideoPlayer with a new one.
This solution roundabouts the whole question in one way. I doesn't solve how to change video of video_player or chewie. Instead it follows the documented principal on how to use chewie for the whole life cycle of a host widget (MyVideoPlayer) and swap that one out to change video url.
You can stuff in more things in the outer widget as you see fit if you don't want to dedicate it just to containing MyVideoPlayer. Ie. if you want a description text adjacent to it based on app state.
Outer Widget
I write with this. but it can be omitted in Dart code.
class QuizVideoPlayer extends StatefulWidget {
#override
_QuizVideoPlayerState createState() => _QuizVideoPlayerState();
}
class _QuizVideoPlayerState extends State<QuizVideoPlayer> {
Word _url;
UniqueKey _urlKey;
// Call this method from button or in reaction to model change etc.
// I call it from Provider.of in didChangeDependencies, but I don't think it is
// a necessary detail of the answer as it depends on how you do state management.
// The key in this solution is that state management occur in the outer widget and
// due to some trigger call _changeUrl() which changes _url and _urlKey which then
// swaps out MyVideoPlayer.
#override
void _changeUrl(String newUrl) async {
this.setState(() {
// Rebuild MyVideoPlayer with a new instance => eventually dispose old controllers
this._url = newUrl;
this._urlKey = UniqueKey();
});
}
#override
Widget build(BuildContext context) {
return
/* ... */
this._url != null
? MyVideoPlayer(
this._url,
this._urlKey,
)
: AspectRatio(
aspectRatio: 3 / 2,
child: Container(color: Colors.black),
)
/* ... */
);
}
}
MyVideoPlayer
I write with this. but it can be omitted in Dart code.
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';
class MyVideoPlayer extends StatefulWidget {
final String videoUrl;
final UniqueKey newKey;
MyVideoPlayer(this.videoUrl, this.newKey): super(key: newKey); // passing Unique key to dispose old class instance and create new with new data
#override
_MyVideoPlayerState createState() => _MyVideoPlayerState();
}
class _MyVideoPlayerState extends State<MyVideoPlayer> {
VideoPlayerController _controller;
ChewieController _chewie;
#override
void initState() {
this._initControllers(this.widget.videoUrl);
super.initState();
}
void _initControllers(String url) {
this._controller = VideoPlayerController.network(url);
this._chewie = ChewieController(
videoPlayerController: this._controller,
autoPlay: true,
);
}
#override
void dispose() {
this._controller?.dispose();
this._chewie?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Chewie(controller: this._chewie);
}
}
Related
i have an app that renders a webpage inside a webview. this actually has a bunch of crypto addresses.
I want them to be automatically made clickable. and when they are clicked - i want to show a popup (some information about the addresses).
can this be done ?
im very unsure if this thing about changing the UI is possible...but in desktop web world, there are extensions that do this. if there are any examples of flutter webview codebases that do this, that would be helpful
the second point - communicating back and forth with the webpage is even more confusing. can this be done at all ? can i receive the data of the click back to main flutter app and then do something ?
Here's a working example
This is achieved as I said by Injecting Js when the webpage loads, as for communication with Flutter, I used the flutter Webview plugin provided JavascriptChannel
The Javascript code looks for a specific element firstly on Page load and secondly while scrolling the webpage (to account for newly created dynamic elements)
Here's how the flow works
JS: assigns the element a new css style (Or in your case make it look like a button) or even create a button and insert it into the webpage
JS: assign on click to the element to call the Flutter JS Channel.
Flutter: Receive message Display a snackbar - you can deeplink or do whatever you want.
As the comments on the JS code say. the scrolling behavior calls every time which is not always ideal, you can use another function make it only trigger on a specific scroll distance
Full working example
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
dynamic ctrl;
// if you have multiple elements, just use querySelector All and loop
const jsCodeToForAnElement = """
// Choose the element
watch();
// I would add dealy to calculate delta between scrolls
// Meaning this code wont watch on every scroll
window.onscroll = watch();
function watch(){
let elm = document.querySelector("main h1");
try{
// Style it
elm.style.cssText = "background: red";
// Add on click
elm.onclick = (event)=> {
var walletHTMLElement = event.target;
// Use native API to communicate with Flutter
jsMessager.postMessage('Wallet clicked: ' + walletHTMLElement.innerHTML);
};
}catch(e){
jsMessager.postMessage('Error: ' + e);
}
}
""";
void main() {
runApp(
MaterialApp(
home: WebViewExample(),
),
);
}
class WebViewExample extends StatefulWidget {
#override
WebViewExampleState createState() => WebViewExampleState();
}
class WebViewExampleState extends State<WebViewExample> {
#override
void initState() {
super.initState();
// Enable virtual display.
if (Platform.isAndroid) WebView.platform = AndroidWebView();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: WebView(
initialUrl: 'https://flutter.dev/',
debuggingEnabled: true,
onWebViewCreated: (WebViewController webViewController) {
ctrl = webViewController;
},
javascriptMode: JavascriptMode.unrestricted,
javascriptChannels: <JavascriptChannel>{
JavascriptChannel(
name: 'jsMessager',
onMessageReceived: (jsMessager) async {
if (jsMessager.message.contains("Wallet")) {
var snackBar = SnackBar(
content: Text(jsMessager.message),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
// Do Other Thing like deeplinking to crypto app
// Deeplink to wallet LaunchApp(wallet) <-- clean wallet string first
}
}),
},
onPageStarted: (String url) async {
ctrl.runJavascript(jsCodeToForAnElement);
},
onPageFinished: (String url) async {},
),
);
}
}
Original Answer
This would be possible by Injecting Javascript into Webviews (This is one idea)
1 - I would wait for the page to load
2 - Modify the HTML content using Javascript
Should be pretty straight forward.
Refer to this Answer to see how it is done in Flutter.
https://stackoverflow.com/a/73240357/6151564
So I have this button that navigates to a new view:
RaisedButton(
child: const Text('Advertise device', style: TextStyle(fontSize: 20)),
onPressed: () async {
Nearby().askExternalStoragePermission();
var navigationResult = await Navigator.push(context, MaterialPageRoute(builder: (context) => EndpointList(endpointList)));
endpointList = navigationResult;
Nearby().stopAdvertising();
print("stopped advertising");
}
),
The class implementing the view is declared with with WidgetsBindingObserver so that I can observe the view:
#override
void initState(){
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
When navigating to the view I want to call _advertiseDevice();. For some reason (that suits me), the AppLifecycleState.resumed gets toggled, so instead of calling _advertiseDevice(); in the initState() it gets called here:
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if(state == AppLifecycleState.detached || state == AppLifecycleState.inactive || state == AppLifecycleState.paused){
print("stopped advertising");
Nearby().stopAdvertising();
}else if (state == AppLifecycleState.resumed){
print("advertising because state resumed");
_advertiseDevice();
}
}
This is nice. View opens, and the _advertiseDevice(); does his thing.
Now, I did the same with another view. Also called via a button:
RaisedButton(
child: const Text('Discover devices', style: TextStyle(fontSize: 20)),
onPressed: () async {
await Nearby().askLocationPermission();
var navigationResult = await Navigator.push(context, MaterialPageRoute(builder: (context) => AdvertiserList(advertisingList)));
advertisingList = navigationResult;
Nearby().stopDiscovery();
print("stopped discovering");
},
),
and also declared with with WidgetsBindingObserver to observe the view:
#override
void initState(){
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
with a smilar didChangeAppLifecycleState:
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if(state == AppLifecycleState.detached || state == AppLifecycleState.inactive || state == AppLifecycleState.paused){
print("stopped discovery");
Nearby().stopDiscovery();
}else if (state == AppLifecycleState.resumed){
print("discovering because state resumed");
_discoverDevices();
}
}
Here however, AppLifecycleState.resumed is not called when navigating to the view, like in the previous view. Yes, I can put the _discoverDevices(); in the initState() and it would work just fine (the warning of "already discovering" that pops is irrelevant to me), but I'd like to know why.
Why two classes that implement the same WidgetsBindingObserver logic behave differently?
PS: I'm not adding the code for both classes because I'm still working on it and it's a mess and because (correct me if I'm mistaken), the actions performed by the view is irrelevant for this. This is related to the focus and unfocus of a view, and the actions taken accordingly.
EDIT: Adding the flow for both screens.
The app opens and displays a tab view. Within the first tab there are a couple buttons, one is the one that displays the first screen. Displaying it triggers AppLifecycleState.resumed and the _advertiseDevice(); method is called. (Why? No idea, the app lifecycle didn't change).
The app opens and displays a tab view. Select the second tab. Within the second tab there are a couple buttons, one is the one that displays the second screen. Displaying it does not trigger anything, unlike the first screen. No idea why the difference in behaviour.
I am building a comics app. In order to make comic chapters load faster. I want images to load one by one consequtively. When an image finishes loading, it is presented directly into a pageview. I do not know how to load images one by one and edit the page view to present them.
In order to make load image faster you can try use cached_network_image package like this:
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/350x150",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
It will show placeholder until the image loaded, so the user can see how many image there is and wait until it loaded, and errorWidget to show if some image were getting trouble. So I suggest to use it, it's easier. Unless you want some complicated code you can try use ScrollController put it inside your ListView.builder. Initialize your scroll controller in initState like this :
final _scrollController = ScrollController();
#override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
// scroll has reach end, now load more images.
loadMore();
}
});
}
You can't use setState inside initState so that's why I create another method to call it like this:
Future<void> loadMore() async {
final response = await api.get("curated?per_page=50&page=$_currentPage");
if (response.statusCode == 200) {
var tempList = Pages.fromJson(response.data);
setState(() {
isLoading = false;
wallpaper.addAll(tempList.photos);
});
}
return null;
}
So there you go I hope you can understand, and yes it is more complicated, so like I said I suggest you use CachedNetworkImage.
use a StreamBuilder it will do all the work for u.
I plan to show a banner ad for my app on the top of the screen. When the ad is showing (the user opens the app with an active internet connection) i use Padding Widgets as BottomNavigationItems to place my data below the banner ad. If the user opens the app with no active internet connection (either wifi or mobile data) I get the Ad failed to load : 0 instead of Ad failed to load : 2 which is the official error code for network errors according to https://support.google.com/admob/thread/3494603?hl=en.
Now ive tried to handle if the ad is loaded or not programatically:
void _showBannerAd() async {
_bannerAd = BannerAd(
adUnitId: AdManager.bannerAdUnitId,
size: AdSize.banner,
targetingInfo: _mobileAdTargetingInfo);
bool loaded = await _bannerAd.load();
if (loaded) {
print('success load');
} else {
print('fail load');
}
bool showing =
await _bannerAd.show(anchorOffset: 80.0, anchorType: AnchorType.top);
if (showing) {
print('sucess show');
} else {
print('fail show');
}
}
which i load
#override
void initState() {
_showBannerAd();
super.initState();
}
But even when there is no internet connection the bool values are false although they shouldnt?
Any help please! I want the user to have a good experience - even with 1 Ad in my entire app it looks weird if the ad is not showing and all the data is still padded.
Thanks guys!
Ive finally found an answer to my own question after searching for a while.
Turns out every Admob ad has a listener attribute!
So what i did is
I created a static bool variable in my BottomNavPageState (BottomNavPage is a Statefulwidget) assuming that the user somehow has an internet connection at startup:
static bool adError = false;
Also every BottomNavItemPage is a StatefulWidget. When initialising my routes i just pass the adError variable (at startup) into all the routes which use Ads for now:
List<Widget> _routes = [
TodayPage(),
PlanPage(adError: adError),
TodoPage(adError: adError),
ProgressPage()
];
When intialising my Banner ad i add the Listener to the banner:
void _showBannerAd() async {
_bannerAd = BannerAd(
adUnitId: AdManager.bannerAdUnitId,
size: AdSize.banner,
targetingInfo: _mobileAdTargetingInfo,
listener: (event) {
if (event == MobileAdEvent.failedToLoad) {
setState(() {
adError = true;
_routes = [
TodayPage(),
PlanPage(adError: adError),
TodoPage(adError: adError),
ProgressPage()
];
});
}
});
}
If the ad failes to load i set the State of my BottomNavPageState in which i turn the bool variable to true and reinitialise my routes with the new updated bool.
Also dont forget to add the adError variable to the BottomNavItemPage:
class TodoPage extends StatefulWidget {
final bool adError;
TodoPage({this.adError});
#override
_TodoPageState createState() => _TodoPageState();
}
...
and access the variable to make simple if else check to return the given Padding:
...
Padding(
padding: widget.adError
? EdgeInsets.only(left: 8.0, right: 8.0)
: EdgeInsets.only(top: 60.0, left: 8.0, right: 8.0),
child:
...
And i have to say it works like a charm!
I'm currently working on a flutter app with a websocket.
I want to change the current screen with a Navigator.pushNamed(context, '/path') in the method that is triggered by the websocket on a new message. The problem is that I don't have any context in this method.
So how to change the current screen from this method ?
I dont know if I'm thinking in the wrong way or if I just don't understand something.
Here is the method that is triggered on a new websocket message, his class is not a widget. This object is contained in every screen I create but his variable _parent is always set to match the active screen.
onMessageReceived(data) {
print("new message: " + data + " !");
data = jsonDecode(data);
data.forEach((key, value) {
switch (key) {
case "state":
_parent.newState(value);
break;
}
});
}
Here is the method of the widget:
newState(state){
if(state == "start"){
Navigator.pushNamed(, "/path");
}
}
Thanks in advance
You can use a stream for that. You can register the websocket response, and when it returns you can add it to the stream, and then on your screen you use a StreamBuilder to subscribe to the stream and navigate when needed. Something like this:
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
// Create a stream to receive the values
var webSocketStream = new BehaviorSubject<String>();
onMessageReceived(data) {
print("new message: " + data + " !");
data = jsonDecode(data);
data.forEach((key, value) {
switch (key) {
case "state":
// Write to the stream
webSocketStream.add(value);
break;
}
});
}
class CheckWebSocket extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<String>(
// Subscribe to the stream
stream: webSocketStream,
builder: (context, snapshot) {
// When the stream changes navigate
if (snapshot.hasData && snapshot.data == "start") {
Navigator.pushNamed(context, "/path");
} else {
return Container();
}
}),
);
}
}
I used rxdart for that, because it makes dealing with streams simpler. I also recommend that you extract the code that reads the websocket to a separate class, I put everything in one file just to make it simpler to understand.