Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Faster loading without blob or why does UnityLoader load the build as a blob?

Discussion in 'Web' started by Tschoartschi, Oct 12, 2016.

  1. Tschoartschi

    Tschoartschi

    Joined:
    Apr 15, 2016
    Posts:
    4
    Hey!

    Loading time is very crucial for us and we are not fully satisfied with Unity's performance in this regard. This is why I did some research on speeding up the loading process of our Unity build. Beside the fact that the build is 25mb (unzipped) of JavaScript (and there is nothing we could do, to further trim it down), I tried different ways of loading the build.

    Unity loads the build as a blob and in my benchmarks this was the slowest way of loading the build (to be correct, the main time is not spent on "loading", we cache our js files, it is spent on interpreting/compiling the huge js file). The fastest way was to include a script tag with an async attribute at the end of the index.html file. I saw that the UnityLoader does some checking if Math.fround is available and if not the UnityLoader edits the blob in some fancy way. So I benchmarked the idea of adding the script tag dynamically via JavaScript to the index.html file. Doing it this way you could include a different file if Math.fround wouldn't be available. This method is also faster than the regular Unity approach.

    On my machine adding a script tag with src "to the file" instead of the blob is about 12% faster than the blob variant. It is 11.532,94ms vs 10.132,78ms in average. You can find the detailed measurements here.

    I only did the measurements on Chrome because Firefox always crashed when I opened the console. I think this is due to the fact that Firefox deactivates asm.js if the console is opened. Further I remember that the "blob-approach" was introduced in some version of Unity. This is why I wonder if I miss something and therefore don't see the benefit of loading the build as a blob. So I didn't want to spend too much time on benchmarking something which is maybe not feasible or a bad approach. This is why some questions came up my mind:
    • Maybe loading the build as a blob has something to do with cross browser compatibility or does it has any benefit I don't see?
    • Would a change, away from blob, matter if we only target modern none mobile browser (>= IE11)?
    • Does it make sense to alter the UnityLoader.js file if it's improving performance for us (I'm always sceptical to modify predefined files because most of the time the developers have a good reason of doing it the way they do it)?
    Hope someone could help me clarify these questions so that we can find the best way of loading the Unity build for us.

    Thanks a lot
     
    yuliyF likes this.
  2. alexsuvorov

    alexsuvorov

    Unity Technologies

    Joined:
    Nov 15, 2015
    Posts:
    327
    Hello Tschoartschi.

    Yes, this method has been already considered. In your specific case it might indeed bring about 10% improvement (which makes sense, as the script is being parsed while being downloaded via the script tag), while usually the improvement is not significant, considering the additional complications that come with this approach:

    - you can only serve uncompressed script or should have a proper server setup which serves compressed file with the Content-Encoding header appended, which will cause complications with the server setup for most users.

    - try to properly handle all the situations when the script inside the tag has not been loaded, cover all the possible reasons including: file being not served (onload wont execute on 404), CORS exception, script error etc.

    - you can no longer manually post-process the downloaded script (can not use agent-specific optimizations, can not use custom compression method, can not manually cache the downloaded script in the database)

    Also, for most projects the .data file is much bigger than the .js file, so even if you save some % on loading the .js, normally you will still have to wait for the .data to download, which makes the total loading time exactly the same.

    Nevertheless, in some special cases using a script tag can indeed bring some benefit. Feel free to modify the loader to fit your specific needs. I believe in some cases you should even be able to achieve this through the WebGL template.
     
    Last edited: Oct 12, 2016
  3. Tschoartschi

    Tschoartschi

    Joined:
    Apr 15, 2016
    Posts:
    4
    Hello alexsuvorov!
    Thanks for your response.
    I don't see this as a problem, turning on gzip or similar is quite simple isn't it? We had no problems doing so. I think every serious developer should be able to turn gzip on or ask somebody for help. I think it is even more performant if the browser is doing the decompression and not a JS-script.

    Using a blob to circumvent CORS also sounds very dangerous. I mean, CORS is here for security not to make things harder or annoy people!

    Caching is not our real problem. It's the parsing of the huge JS file. This is why I'm not sure if it is a good idea to post-process 25MB of JavaScript in the browser. Wouldn't it be better to create multiple builds and just include the best build for the specific user-agent? I mean, processing 25MB JavaScript is a very computational heavy task. This would freeze the UI for seconds.

    Our .data file is 4.6MB, the .mem file is 2.1MB and the .js file is 25(!!!)MB.

    Our biggest problem is that the huge JS file causes a long UI stall. Most of the users think the page just crashed and leave it after some seconds. JS file size matters, it's not just about download-time, it's also about compiling/parsing/interpreting time. There is an estimation which says that 1kb of JS results in 1ms UI stall on an average phone (discussion is here). And the same, (of course with other numbers), apply to all devices and computers. This is why we try to find a better solution for the parsing problem and it seems that adding the real .js file with a script tag is better.

    I'm not really convinced that the blob solution is beneficial for anybody.
     
  4. alexsuvorov

    alexsuvorov

    Unity Technologies

    Joined:
    Nov 15, 2015
    Posts:
    327
    If you get 25MB of uncompressed JavaScript code for Release build then you probably have disabled the Strip Engine Code build option (i.e. a simple project with physics disabled should generate about 10 MB of uncompressed JavaScript in the latest beta). Which version of Unity are you using?

    The http://beta.unity3d.com/jonas/AngryBots/ demo (which has been built with Unity 5.0.0) represents an average game and got:
    4 MB of compressed JavaScript (19 MB uncompressed)
    15 MB of compressed asset data (31 MB uncompressed)
    So, basically the data file download time in this case is about 4 times bigger, which makes the parsing difference achieved by using a script tag irrelevant.

    You are absolutely right, the blob solution is not beneficial for everybody (while it does not introduce additional delay in most cases either), but it resolves a lot of problems that developers often run into. For example, you've had no issues with setting up your server, while other developers might use not very expensive hosting and various storages to host WebGL content, which means that in some cases they don't even have any access to the response headers, or don't have sufficient permissions to adjust it. For this reason the script tag can only be considered to be an option, but not a replacement of the current approach.

    By the way, you have pointed out a very important thing, which is that developers with deep experience in web development and server setup are indeed more likely to require some adjustments for the loading process, and would like to have a different setup which can be much more easily adjusted. Currently the problem is that sometimes you need to make some changes to the UnityLoader to fit your needs, which is a bit problematic as a new UnityLoader is generated each time and it is specific to the build (i.e. contains offsets and GUID for the data file etc.).

    We are currently working on another approach, which will bring multiple changes: provide an easy way to embed a build on your page, provide code namespacing for each embedded build (so that you can embed more than one build on the same page without iframes), make the loader independent from the build (basically UnityLoader will be the same for any WebGL build). This should make it much easier for developers to adjust the loading process to fit their specific needs. You can check the full description here https://forum.unity3d.com/threads/new-unity-webgl-embedding-api-white-paper.430909/

    P.S. Regarding the script tag, the following problems are still not fully solved:
    - How would you handle a situation when the script inside the tag has not been loaded? (for example, you may start a HEAD XMLHttpRequest for the same script url in parallel and check if it returns 404, but this is still not 100% reliable, as well as having a loading timeout, and onerror is not supported in IE if i am not mistaken)
    - How would you monitor the download progress of the script in the tag? (considering that in your specific case script download takes the most time)
    - How would you reliably perform namespacing for the code loaded via the script tag, so that it does not interfere with rest of the page? (current approach is to wrap the downloaded code inside a function and assign it to a unique object generated at runtime, while fixed script code can only have this object name hardcoded in advance, which does not fully guarantee uniqueness)
     
    Last edited: Oct 14, 2016
  5. JakeOfSpades

    JakeOfSpades

    Joined:
    Aug 22, 2015
    Posts:
    44
    I am very interested in this topic, I am struggling with this issue as well. It takes about ~40 seconds to load my game which is unacceptable IMO as I feel most users will leave by that time.
     
  6. danielesuppo

    danielesuppo

    Joined:
    Oct 20, 2015
    Posts:
    331
    Hi Tschoartschi, I'm experiencing the same parsing time issue. Any chance to know this script you added? I'm not a developer so it's deep night to me :)
     
  7. alexsuvorov

    alexsuvorov

    Unity Technologies

    Joined:
    Nov 15, 2015
    Posts:
    327
    Hello danielesuppo.

    You can easily check if loading the script via tag brings significant benefit for your build by just making a few changes to the index.html (just make sure you have properly configured your server to host compressed content with Content-Encoding: gzip header appended). The code below has been tested with Unity 5.4 builds.

    Important note: when running in Chrome, the loader first parses the downloaded asm.js module and optimizes out Math.fround calls. This optimization should make your build have better performance in Chrome. However, loading the script via tag will bypass this optimization (the build will load faster but will work slower in Chrome). Ideally, you can host both versions of the module (normal and optimized for Chrome) and serve it depending on the user agent (as Tschoartschi suggested). But as we are now only interested in comparison between blob and tag script loading methods, the default optimization will be simply disabled by overriding the LoadJSCode function (see below) and therefore should not affect the results.

    Most of the loader functionality can be overridden, for example, from a custom compatibilitycheck handler (as it is executed after the UnityLoader.js has been already parsed). Also, lets add an onRuntimeInitialized handler which will help us monitor the total loading and initialization time, that we will use for comparison (you may just copy the following code and replace the "Release/build.*" lines with the corresponding "Release/your-build-name.*"):
    Code (JavaScript):
    1. <script type='text/javascript'>
    2.   var startTime = new Date().getTime();
    3.   var Module = {
    4.     TOTAL_MEMORY: 268435456,
    5.     errorhandler: null,            // arguments: err, url, line. This function must return 'true' if the error is handled, otherwise 'false'
    6.     compatibilitycheck: function () {
    7.       LoadJSCode = function (code) {
    8.         LoadJSCodeBlob(new Blob([code], { type: 'text/javascript' }));
    9.       };
    10.       return CompatibilityCheck();
    11.     },
    12.     onRuntimeInitialized: function() {
    13.       console.log("the runtime is ready after: " + (new Date().getTime() - startTime) + " ms");
    14.     },
    15.     dataUrl: "Release/build.data",
    16.     codeUrl: "Release/build.js",
    17.     memUrl: "Release/build.mem",
    18.  
    19.   };
    20. </script>
    21. <script src="Release/UnityLoader.js"></script>

    Now we can override the LoadCompressedJS in order to load the sctipt via tag. Depending on your Unity version you might try one of the following approaches:

    a) create a script tag dynamically from the overridden LoadCompressedJS function:
    Code (JavaScript):
    1. <script type='text/javascript'>
    2.   var startTime = new Date().getTime();
    3.   var Module = {
    4.     TOTAL_MEMORY: 268435456,
    5.     errorhandler: null,            // arguments: err, url, line. This function must return 'true' if the error is handled, otherwise 'false'
    6.     compatibilitycheck: function () {
    7.       LoadCompressedJS = function(url) {
    8.         var script = document.createElement("script");
    9.         script.onerror = function() { alert(url + " has not been loaded"); };
    10.         script.src = url;
    11.         document.body.appendChild(script);
    12.         console.log("script download started after: " + (new Date().getTime() - startTime) + " ms");
    13.       };
    14.       return CompatibilityCheck();
    15.     },
    16.     onRuntimeInitialized: function() {
    17.       console.log("the runtime is ready after: " + (new Date().getTime() - startTime) + " ms");
    18.     },
    19.     dataUrl: "Release/build.data",
    20.     codeUrl: "Release/build.js",
    21.     memUrl: "Release/build.mem",
    22.  
    23.   };
    24. </script>
    25. <script src="Release/UnityLoader.js"></script>
    b) or add the script tag directly into the index.html and suppress the LoadCompressedJS (might not work in some Unity versions):
    Code (JavaScript):
    1. <script type='text/javascript'>
    2.   var startTime = new Date().getTime();
    3.   var Module = {
    4.     TOTAL_MEMORY: 268435456,
    5.     errorhandler: null,            // arguments: err, url, line. This function must return 'true' if the error is handled, otherwise 'false'
    6.     compatibilitycheck: function () {
    7.       LoadCompressedJS = function (url) {};
    8.       return CompatibilityCheck();
    9.     },
    10.     onRuntimeInitialized: function() {
    11.       console.log("the runtime is ready after: " + (new Date().getTime() - startTime) + " ms");
    12.     },
    13.     dataUrl: "Release/build.data",
    14.     codeUrl: "Release/build.js",
    15.     memUrl: "Release/build.mem",
    16.  
    17.   };
    18. </script>
    19. <script src="Release/build.js" onerror="alert(src + ' has not been loaded');" async></script>
    20. <script src="Release/UnityLoader.js"></script>
    Note that downloaded files might get cached by the browser, so make sure you perform all the tests in a clean environment (i.e. cleanup the cache before each test, otherwise the build which has been run first will always load somewhat slower).

    Average tests results for the AngryBots (built with 5.4):

    [Chrome]
    original: 17.7 sec
    dynamic script tag: 16.8 sec
    static script tag: 16.7 sec

    [Firefox]
    original: 16.2 sec
    dynamic script tag: 16.2 sec
    static script tag: 16.1 sec

    Here we can see that Tschoartschi is right and loading the code via script tag might indeed save you a second for initial download in Chrome. Though if you decide to go this way, don't forget about Math.fround optimizations, which are required to preserve the build performance in Chrome (you will basically need two versions of the code to be served then, as Firefox requires the original version of the module code in order to run fast).
     
    Last edited: Oct 16, 2016
    danielesuppo likes this.
  8. danielesuppo

    danielesuppo

    Joined:
    Oct 20, 2015
    Posts:
    331
    Many thanks Alex Suvorov, I'll try soon
     
  9. Tschoartschi

    Tschoartschi

    Joined:
    Apr 15, 2016
    Posts:
    4
    @alexsuvorov Thanks for the detailed explanation and for the effort you put into Unity :)

    We are using Unity 5.4 and we need some parts of the physics-engine (and therefore the whole physics-engine becomes part of our JS blob...). I'm pretty sure we can not trim down the build size much further. Maybe some KB but the vast amount comes from Unity (and physics).

    I think that also the parsing is a problem. In our application the parsing causes an UI stall. Everything is freezed until parsing is done. This is not a big problem on top notch hardware but on old devices the UI freezes very long and most of these users just close the browser and will never come back. So parsing is a huge topic for us! The downloading times can be concealed by good UX, so the perceived performance could be OK. But a UI stall prevents every trick and leaves us chancels.
    This sounds interesting, although I'm not super convinced by the idea of replacing the js extension with the proprietary unityweb file extension. I'm a web developer and I think it makes sense to rely on browser standards, browser APIs etc. Because if it is compliant to the browsers, you can leverage all the progression which comes with the improvements of the browsers. Therefore I'm not sure if it is ideal to do things like decompression in JS and don't let it up to the browser how decompression is done. But as pointed out by alexsuvorov, there are many challenges and problems and I understand that it is not always easy to solve them in a, let's call it a, "browser-way".

    For point one and two you could try to request the file via AJAX and set the content and accept headers to javascript.But I'm not sure how parsing is done, when requesting a script via AJAX. I would have to try it out. Maybe this would lead to the same "problems" as with the blob solution. Maybe I'll give it a try when I have time.

    Ad point three: I think namespacing is hard and I also have no general idea what would be the best approach

    Yeah we really like the possibilities which Unity gives us, if we could trim down the JS blob, Unity would be almost perfect for us :)
     
  10. jonas-echterhoff

    jonas-echterhoff

    Unity Technologies

    Joined:
    Aug 18, 2005
    Posts:
    1,666
    I think most of the relevant things have been said by Alex already: Yes, it is possible that using a script tag is beneficial in some cases - we have been benchmarking this option, and did not find the benefits to outweigh the downsides (it is not always measurably faster) - and if it does help you in your case, then feel free to modify your build to use it.

    Note that you should probably not make assumptions like this:

    We used to think similar, but, despite all our attempts to document this, we found that we were helping people with this on a daily base, and moreover, found that people were frequently shipping life content with no compression whatsoever - and would then complain about unacceptable build sizes. This workaround has greatly improved the initial experience of Unity WebGL to new developers, and is very important for that reason.

    As for your JS code size: In 5.4 and higher, you can go try our experimental build report feature at http://files.unity3d.com/build-report/ . If you click on the "Stripping" tab, this lets you profile what contributes to the size of your code, and might help finding some additional areas to optimize.
     
  11. stephanwinterberger

    stephanwinterberger

    Joined:
    Aug 22, 2016
    Posts:
    27
    I stumbled across this post because we have the same problem. Also our UI blocks when the Unity JS blob is interpreted. I think that there are many users which have similar issues. For example: https://forum.unity3d.com/threads/unityloader-js-blocking-question.438494/ and several others.

    I'm not sure how I should interpret this comment. What is the target audience of Unity? For me this sounds like that script kiddies are very important. For professional users this sounds somehow bad. Professionals get worse performance because some script kiddies can not turn on gzip and therefore spam the Unity support channel. I'm not sure if this is the best way. Okay but that's just my opinion.

    @jonas-echterhoff is there any chance to get rid of the UI stall? Or do we have to live with it? Are there any tricks we could do which are not activated by default? Can we disable something which is turned on by default because it makes sense for script kiddies?
     
  12. jonas-echterhoff

    jonas-echterhoff

    Unity Technologies

    Joined:
    Aug 18, 2005
    Posts:
    1,666
    I have two comments about this.

    1. as I wrote above, we could not measure any better startup performance when using script tags, so the tradeoff where "script kiddies" benefit on the cost of professionals here is purely hypothetical.

    2. No, I don't think that this only benefits "script kiddies". I frequently see professional game studios publish games with the compression not properly set up on their servers. And even if people can set this up yourselves, it is still convenient for all our users if they don't have to. Which is, after all, the whole philosophy behind Unity. Sure, we can all write our own game engine, being professionals and everything. But the fact that we don't have to makes live a bit easier :)
     
  13. jonas-echterhoff

    jonas-echterhoff

    Unity Technologies

    Joined:
    Aug 18, 2005
    Posts:
    1,666
    And, no, I am not aware of any way to get rid of the UI stall while the browser is compiling the code, which is feasible today, and does not have any severe drawbacks.
     
  14. stephanwinterberger

    stephanwinterberger

    Joined:
    Aug 22, 2016
    Posts:
    27
    @jonas-echterhoff I hope you didn't feel offended by my previous post. I was kind of "frustrated" yesterday when I had to explain to my boss why our game loads so slow and why it crashes on so many devices. I really appreciate the work of Unity, but we are not really satisfied with the performance in the web (downloading time, build sizes, startup times etc).

    I didn't measure it myself but the explanation of @Tschoartschi do make sense to me. I'm originally a web guy and in my experience it is always the best to do things in a "standard" way or in a way how the browser expects it. This was the reason why I trusted @Tschoartschi without measuring it myself.

    Yes I understand your point and maybe that's the price we have to pay in the web if we use a multi-platform engine. The question what should be done by the engine and what should be implemented and adjusted by the programmer is a hard one. I don't think gzip is Unities responsibility but as you pointed out, you see it differently. I think build size is a huge concern, you think that's not a big deal because in most cases the asset data is much bigger.

    To bring this to an end, for us the webgl export is far from being awesome but we like the possibilities which Unity offers us. So we hope that Unity will we great for the web in the near future. And since there are many smart people at Unity I'm sure you guys will rock the web platform soon.
     
  15. danielesuppo

    danielesuppo

    Joined:
    Oct 20, 2015
    Posts:
    331
    Hello!
    I don't have a big data file (2mb), but I have a bigger js file (6mb), and expecially on older devices I have a very long parsing time (above 1 min).
    More, I use some big textures, so my build would need at least 128Mb memory size, that would crash on many devices, due to browser fragmented memory, open tabs, etc.

    So:
    1) I've used the solution suggested by alexsuvorov with a dynamic script tag (the static doesn't work for me), and the parsing time decreased (about 20% both on PC and on mobile devices).

    2) I've splitted my project in 2 versions: one for PC, another for mobile: usually on mobiles you don't need really big textures, so I could decrease any big texture about 1/4, and allocate less memory only for mobile version.

    With these 2 solutions combined togheter I think to be pretty happy about loading & parsing time, on any device (for sure on older devices it's still a bit a pain because of UI block, but not too much), and really happy about compatibility

    This is the index.html to load 2 differents versions of the release
    Code (JavaScript):
    1.  
    2. <script>
    3.     var md = new MobileDetect(window.navigator.userAgent);
    4.     if (md.mobile()||md.phone()||md.tablet()) {
    5.         var device=1;
    6.         var Module = {
    7.             TOTAL_MEMORY: 33554432,
    8.             errorhandler: null,            // arguments: err, url, line. This function must return 'true' if the error is handled, otherwise 'false'
    9.             compatibilitycheck: function () {
    10.       LoadCompressedJS = function(url) {
    11.         var script = document.createElement("script");
    12.         script.onerror = function() { alert(url + " has not been loaded"); };
    13.         script.src = url;
    14.         document.body.appendChild(script);
    15.                 };
    16.       return CompatibilityCheck();
    17.     },
    18.         dataUrl: "Release_mobile/release.datagz",
    19.             codeUrl: "Release_mobile/release.jsgz",
    20.             memUrl: "Release_mobile/release.memgz",
    21.         };
    22.         document.write('<scri'+'pt src="Release_mobile/UnityLoader.js"></'+'script>');  
    23.     } else {
    24.         var device=0;
    25.         var Module = {
    26.             TOTAL_MEMORY: 67108864,
    27.             errorhandler: null,            // arguments: err, url, line. This function must return 'true' if the error is handled, otherwise 'false'
    28.                 compatibilitycheck: function () {
    29.       LoadCompressedJS = function(url) {
    30.         var script = document.createElement("script");
    31.         script.onerror = function() { alert(url + " has not been loaded"); };
    32.         script.src = url;
    33.         document.body.appendChild(script);
    34.                 };
    35.       return CompatibilityCheck();
    36.     },
    37.             dataUrl: "Release_PC/release.datagz",
    38.             codeUrl: "Release_PC/release.jsgz",
    39.             memUrl: "Release_PC/release.memgz",
    40.         };
    41.         document.write('<scri'+'pt src="Release_PC/UnityLoader.js"></'+'script>');
    42.      
    43.     }
    44.   </script>
    45.  
    and this is the "mobile-detect.js" file that you should put in "TemplateData" folder
    Code (JavaScript):
    1. // THIS FILE IS GENERATED - DO NOT EDIT!
    2. /*!mobile-detect v1.3.3 2016-07-31*/
    3. /*global module:false, define:false*/
    4. /*!@license Copyright 2013, Heinrich Goebl, License: MIT, see https://github.com/hgoebl/mobile-detect.js*/
    5. (function (define, undefined) {
    6. define(function () {
    7.     'use strict';
    8.  
    9.     var impl = {};
    10.  
    11.     impl.mobileDetectRules = {
    12.     "phones": {
    13.         "iPhone": "\\biPhone\\b|\\biPod\\b",
    14.         "BlackBerry": "BlackBerry|\\bBB10\\b|rim[0-9]+",
    15.         "HTC": "HTC|HTC.*(Sensation|Evo|Vision|Explorer|6800|8100|8900|A7272|S510e|C110e|Legend|Desire|T8282)|APX515CKT|Qtek9090|APA9292KT|HD_mini|Sensation.*Z710e|PG86100|Z715e|Desire.*(A8181|HD)|ADR6200|ADR6400L|ADR6425|001HT|Inspire 4G|Android.*\\bEVO\\b|T-Mobile G1|Z520m",
    16.         "Nexus": "Nexus One|Nexus S|Galaxy.*Nexus|Android.*Nexus.*Mobile|Nexus 4|Nexus 5|Nexus 6",
    17.         "Dell": "Dell.*Streak|Dell.*Aero|Dell.*Venue|DELL.*Venue Pro|Dell Flash|Dell Smoke|Dell Mini 3iX|XCD28|XCD35|\\b001DL\\b|\\b101DL\\b|\\bGS01\\b",
    18.         "Motorola": "Motorola|DROIDX|DROID BIONIC|\\bDroid\\b.*Build|Android.*Xoom|HRI39|MOT-|A1260|A1680|A555|A853|A855|A953|A955|A956|Motorola.*ELECTRIFY|Motorola.*i1|i867|i940|MB200|MB300|MB501|MB502|MB508|MB511|MB520|MB525|MB526|MB611|MB612|MB632|MB810|MB855|MB860|MB861|MB865|MB870|ME501|ME502|ME511|ME525|ME600|ME632|ME722|ME811|ME860|ME863|ME865|MT620|MT710|MT716|MT720|MT810|MT870|MT917|Motorola.*TITANIUM|WX435|WX445|XT300|XT301|XT311|XT316|XT317|XT319|XT320|XT390|XT502|XT530|XT531|XT532|XT535|XT603|XT610|XT611|XT615|XT681|XT701|XT702|XT711|XT720|XT800|XT806|XT860|XT862|XT875|XT882|XT883|XT894|XT901|XT907|XT909|XT910|XT912|XT928|XT926|XT915|XT919|XT925|XT1021|\\bMoto E\\b",
    19.         "Samsung": "Samsung|SM-G9250|GT-19300|SGH-I337|BGT-S5230|GT-B2100|GT-B2700|GT-B2710|GT-B3210|GT-B3310|GT-B3410|GT-B3730|GT-B3740|GT-B5510|GT-B5512|GT-B5722|GT-B6520|GT-B7300|GT-B7320|GT-B7330|GT-B7350|GT-B7510|GT-B7722|GT-B7800|GT-C3010|GT-C3011|GT-C3060|GT-C3200|GT-C3212|GT-C3212I|GT-C3262|GT-C3222|GT-C3300|GT-C3300K|GT-C3303|GT-C3303K|GT-C3310|GT-C3322|GT-C3330|GT-C3350|GT-C3500|GT-C3510|GT-C3530|GT-C3630|GT-C3780|GT-C5010|GT-C5212|GT-C6620|GT-C6625|GT-C6712|GT-E1050|GT-E1070|GT-E1075|GT-E1080|GT-E1081|GT-E1085|GT-E1087|GT-E1100|GT-E1107|GT-E1110|GT-E1120|GT-E1125|GT-E1130|GT-E1160|GT-E1170|GT-E1175|GT-E1180|GT-E1182|GT-E1200|GT-E1210|GT-E1225|GT-E1230|GT-E1390|GT-E2100|GT-E2120|GT-E2121|GT-E2152|GT-E2220|GT-E2222|GT-E2230|GT-E2232|GT-E2250|GT-E2370|GT-E2550|GT-E2652|GT-E3210|GT-E3213|GT-I5500|GT-I5503|GT-I5700|GT-I5800|GT-I5801|GT-I6410|GT-I6420|GT-I7110|GT-I7410|GT-I7500|GT-I8000|GT-I8150|GT-I8160|GT-I8190|GT-I8320|GT-I8330|GT-I8350|GT-I8530|GT-I8700|GT-I8703|GT-I8910|GT-I9000|GT-I9001|GT-I9003|GT-I9010|GT-I9020|GT-I9023|GT-I9070|GT-I9082|GT-I9100|GT-I9103|GT-I9220|GT-I9250|GT-I9300|GT-I9305|GT-I9500|GT-I9505|GT-M3510|GT-M5650|GT-M7500|GT-M7600|GT-M7603|GT-M8800|GT-M8910|GT-N7000|GT-S3110|GT-S3310|GT-S3350|GT-S3353|GT-S3370|GT-S3650|GT-S3653|GT-S3770|GT-S3850|GT-S5210|GT-S5220|GT-S5229|GT-S5230|GT-S5233|GT-S5250|GT-S5253|GT-S5260|GT-S5263|GT-S5270|GT-S5300|GT-S5330|GT-S5350|GT-S5360|GT-S5363|GT-S5369|GT-S5380|GT-S5380D|GT-S5560|GT-S5570|GT-S5600|GT-S5603|GT-S5610|GT-S5620|GT-S5660|GT-S5670|GT-S5690|GT-S5750|GT-S5780|GT-S5830|GT-S5839|GT-S6102|GT-S6500|GT-S7070|GT-S7200|GT-S7220|GT-S7230|GT-S7233|GT-S7250|GT-S7500|GT-S7530|GT-S7550|GT-S7562|GT-S7710|GT-S8000|GT-S8003|GT-S8500|GT-S8530|GT-S8600|SCH-A310|SCH-A530|SCH-A570|SCH-A610|SCH-A630|SCH-A650|SCH-A790|SCH-A795|SCH-A850|SCH-A870|SCH-A890|SCH-A930|SCH-A950|SCH-A970|SCH-A990|SCH-I100|SCH-I110|SCH-I400|SCH-I405|SCH-I500|SCH-I510|SCH-I515|SCH-I600|SCH-I730|SCH-I760|SCH-I770|SCH-I830|SCH-I910|SCH-I920|SCH-I959|SCH-LC11|SCH-N150|SCH-N300|SCH-R100|SCH-R300|SCH-R351|SCH-R400|SCH-R410|SCH-T300|SCH-U310|SCH-U320|SCH-U350|SCH-U360|SCH-U365|SCH-U370|SCH-U380|SCH-U410|SCH-U430|SCH-U450|SCH-U460|SCH-U470|SCH-U490|SCH-U540|SCH-U550|SCH-U620|SCH-U640|SCH-U650|SCH-U660|SCH-U700|SCH-U740|SCH-U750|SCH-U810|SCH-U820|SCH-U900|SCH-U940|SCH-U960|SCS-26UC|SGH-A107|SGH-A117|SGH-A127|SGH-A137|SGH-A157|SGH-A167|SGH-A177|SGH-A187|SGH-A197|SGH-A227|SGH-A237|SGH-A257|SGH-A437|SGH-A517|SGH-A597|SGH-A637|SGH-A657|SGH-A667|SGH-A687|SGH-A697|SGH-A707|SGH-A717|SGH-A727|SGH-A737|SGH-A747|SGH-A767|SGH-A777|SGH-A797|SGH-A817|SGH-A827|SGH-A837|SGH-A847|SGH-A867|SGH-A877|SGH-A887|SGH-A897|SGH-A927|SGH-B100|SGH-B130|SGH-B200|SGH-B220|SGH-C100|SGH-C110|SGH-C120|SGH-C130|SGH-C140|SGH-C160|SGH-C170|SGH-C180|SGH-C200|SGH-C207|SGH-C210|SGH-C225|SGH-C230|SGH-C417|SGH-C450|SGH-D307|SGH-D347|SGH-D357|SGH-D407|SGH-D415|SGH-D780|SGH-D807|SGH-D980|SGH-E105|SGH-E200|SGH-E315|SGH-E316|SGH-E317|SGH-E335|SGH-E590|SGH-E635|SGH-E715|SGH-E890|SGH-F300|SGH-F480|SGH-I200|SGH-I300|SGH-I320|SGH-I550|SGH-I577|SGH-I600|SGH-I607|SGH-I617|SGH-I627|SGH-I637|SGH-I677|SGH-I700|SGH-I717|SGH-I727|SGH-i747M|SGH-I777|SGH-I780|SGH-I827|SGH-I847|SGH-I857|SGH-I896|SGH-I897|SGH-I900|SGH-I907|SGH-I917|SGH-I927|SGH-I937|SGH-I997|SGH-J150|SGH-J200|SGH-L170|SGH-L700|SGH-M110|SGH-M150|SGH-M200|SGH-N105|SGH-N500|SGH-N600|SGH-N620|SGH-N625|SGH-N700|SGH-N710|SGH-P107|SGH-P207|SGH-P300|SGH-P310|SGH-P520|SGH-P735|SGH-P777|SGH-Q105|SGH-R210|SGH-R220|SGH-R225|SGH-S105|SGH-S307|SGH-T109|SGH-T119|SGH-T139|SGH-T209|SGH-T219|SGH-T229|SGH-T239|SGH-T249|SGH-T259|SGH-T309|SGH-T319|SGH-T329|SGH-T339|SGH-T349|SGH-T359|SGH-T369|SGH-T379|SGH-T409|SGH-T429|SGH-T439|SGH-T459|SGH-T469|SGH-T479|SGH-T499|SGH-T509|SGH-T519|SGH-T539|SGH-T559|SGH-T589|SGH-T609|SGH-T619|SGH-T629|SGH-T639|SGH-T659|SGH-T669|SGH-T679|SGH-T709|SGH-T719|SGH-T729|SGH-T739|SGH-T746|SGH-T749|SGH-T759|SGH-T769|SGH-T809|SGH-T819|SGH-T839|SGH-T919|SGH-T929|SGH-T939|SGH-T959|SGH-T989|SGH-U100|SGH-U200|SGH-U800|SGH-V205|SGH-V206|SGH-X100|SGH-X105|SGH-X120|SGH-X140|SGH-X426|SGH-X427|SGH-X475|SGH-X495|SGH-X497|SGH-X507|SGH-X600|SGH-X610|SGH-X620|SGH-X630|SGH-X700|SGH-X820|SGH-X890|SGH-Z130|SGH-Z150|SGH-Z170|SGH-ZX10|SGH-ZX20|SHW-M110|SPH-A120|SPH-A400|SPH-A420|SPH-A460|SPH-A500|SPH-A560|SPH-A600|SPH-A620|SPH-A660|SPH-A700|SPH-A740|SPH-A760|SPH-A790|SPH-A800|SPH-A820|SPH-A840|SPH-A880|SPH-A900|SPH-A940|SPH-A960|SPH-D600|SPH-D700|SPH-D710|SPH-D720|SPH-I300|SPH-I325|SPH-I330|SPH-I350|SPH-I500|SPH-I600|SPH-I700|SPH-L700|SPH-M100|SPH-M220|SPH-M240|SPH-M300|SPH-M305|SPH-M320|SPH-M330|SPH-M350|SPH-M360|SPH-M370|SPH-M380|SPH-M510|SPH-M540|SPH-M550|SPH-M560|SPH-M570|SPH-M580|SPH-M610|SPH-M620|SPH-M630|SPH-M800|SPH-M810|SPH-M850|SPH-M900|SPH-M910|SPH-M920|SPH-M930|SPH-N100|SPH-N200|SPH-N240|SPH-N300|SPH-N400|SPH-Z400|SWC-E100|SCH-i909|GT-N7100|GT-N7105|SCH-I535|SM-N900A|SGH-I317|SGH-T999L|GT-S5360B|GT-I8262|GT-S6802|GT-S6312|GT-S6310|GT-S5312|GT-S5310|GT-I9105|GT-I8510|GT-S6790N|SM-G7105|SM-N9005|GT-S5301|GT-I9295|GT-I9195|SM-C101|GT-S7392|GT-S7560|GT-B7610|GT-I5510|GT-S7582|GT-S7530E|GT-I8750|SM-G9006V|SM-G9008V|SM-G9009D|SM-G900A|SM-G900D|SM-G900F|SM-G900H|SM-G900I|SM-G900J|SM-G900K|SM-G900L|SM-G900M|SM-G900P|SM-G900R4|SM-G900S|SM-G900T|SM-G900V|SM-G900W8|SHV-E160K|SCH-P709|SCH-P729|SM-T2558|GT-I9205|SM-G9350|SM-J120F",
    20.         "LG": "\\bLG\\b;|LG[- ]?(C800|C900|E400|E610|E900|E-900|F160|F180K|F180L|F180S|730|855|L160|LS740|LS840|LS970|LU6200|MS690|MS695|MS770|MS840|MS870|MS910|P500|P700|P705|VM696|AS680|AS695|AX840|C729|E970|GS505|272|C395|E739BK|E960|L55C|L75C|LS696|LS860|P769BK|P350|P500|P509|P870|UN272|US730|VS840|VS950|LN272|LN510|LS670|LS855|LW690|MN270|MN510|P509|P769|P930|UN200|UN270|UN510|UN610|US670|US740|US760|UX265|UX840|VN271|VN530|VS660|VS700|VS740|VS750|VS910|VS920|VS930|VX9200|VX11000|AX840A|LW770|P506|P925|P999|E612|D955|D802|MS323)",
    21.         "Sony": "SonyST|SonyLT|SonyEricsson|SonyEricssonLT15iv|LT18i|E10i|LT28h|LT26w|SonyEricssonMT27i|C5303|C6902|C6903|C6906|C6943|D2533",
    22.         "Asus": "Asus.*Galaxy|PadFone.*Mobile",
    23.         "NokiaLumia": "Lumia [0-9]{3,4}",
    24.         "Micromax": "Micromax.*\\b(A210|A92|A88|A72|A111|A110Q|A115|A116|A110|A90S|A26|A51|A35|A54|A25|A27|A89|A68|A65|A57|A90)\\b",
    25.         "Palm": "PalmSource|Palm",
    26.         "Vertu": "Vertu|Vertu.*Ltd|Vertu.*Ascent|Vertu.*Ayxta|Vertu.*Constellation(F|Quest)?|Vertu.*Monika|Vertu.*Signature",
    27.         "Pantech": "PANTECH|IM-A850S|IM-A840S|IM-A830L|IM-A830K|IM-A830S|IM-A820L|IM-A810K|IM-A810S|IM-A800S|IM-T100K|IM-A725L|IM-A780L|IM-A775C|IM-A770K|IM-A760S|IM-A750K|IM-A740S|IM-A730S|IM-A720L|IM-A710K|IM-A690L|IM-A690S|IM-A650S|IM-A630K|IM-A600S|VEGA PTL21|PT003|P8010|ADR910L|P6030|P6020|P9070|P4100|P9060|P5000|CDM8992|TXT8045|ADR8995|IS11PT|P2030|P6010|P8000|PT002|IS06|CDM8999|P9050|PT001|TXT8040|P2020|P9020|P2000|P7040|P7000|C790",
    28.         "Fly": "IQ230|IQ444|IQ450|IQ440|IQ442|IQ441|IQ245|IQ256|IQ236|IQ255|IQ235|IQ245|IQ275|IQ240|IQ285|IQ280|IQ270|IQ260|IQ250",
    29.         "Wiko": "KITE 4G|HIGHWAY|GETAWAY|STAIRWAY|DARKSIDE|DARKFULL|DARKNIGHT|DARKMOON|SLIDE|WAX 4G|RAINBOW|BLOOM|SUNSET|GOA(?!nna)|LENNY|BARRY|IGGY|OZZY|CINK FIVE|CINK PEAX|CINK PEAX 2|CINK SLIM|CINK SLIM 2|CINK +|CINK KING|CINK PEAX|CINK SLIM|SUBLIM",
    30.         "iMobile": "i-mobile (IQ|i-STYLE|idea|ZAA|Hitz)",
    31.         "SimValley": "\\b(SP-80|XT-930|SX-340|XT-930|SX-310|SP-360|SP60|SPT-800|SP-120|SPT-800|SP-140|SPX-5|SPX-8|SP-100|SPX-8|SPX-12)\\b",
    32.         "Wolfgang": "AT-B24D|AT-AS50HD|AT-AS40W|AT-AS55HD|AT-AS45q2|AT-B26D|AT-AS50Q",
    33.         "Alcatel": "Alcatel",
    34.         "Nintendo": "Nintendo 3DS",
    35.         "Amoi": "Amoi",
    36.         "INQ": "INQ",
    37.         "GenericPhone": "Tapatalk|PDA;|SAGEM|\\bmmp\\b|pocket|\\bpsp\\b|symbian|Smartphone|smartfon|treo|up.browser|up.link|vodafone|\\bwap\\b|nokia|Series40|Series60|S60|SonyEricsson|N900|MAUI.*WAP.*Browser"
    38.     },
    39.     "tablets": {
    40.         "iPad": "iPad|iPad.*Mobile",
    41.         "NexusTablet": "Android.*Nexus[\\s]+(7|9|10)",
    42.         "SamsungTablet": "SAMSUNG.*Tablet|Galaxy.*Tab|SC-01C|GT-P1000|GT-P1003|GT-P1010|GT-P3105|GT-P6210|GT-P6800|GT-P6810|GT-P7100|GT-P7300|GT-P7310|GT-P7500|GT-P7510|SCH-I800|SCH-I815|SCH-I905|SGH-I957|SGH-I987|SGH-T849|SGH-T859|SGH-T869|SPH-P100|GT-P3100|GT-P3108|GT-P3110|GT-P5100|GT-P5110|GT-P6200|GT-P7320|GT-P7511|GT-N8000|GT-P8510|SGH-I497|SPH-P500|SGH-T779|SCH-I705|SCH-I915|GT-N8013|GT-P3113|GT-P5113|GT-P8110|GT-N8010|GT-N8005|GT-N8020|GT-P1013|GT-P6201|GT-P7501|GT-N5100|GT-N5105|GT-N5110|SHV-E140K|SHV-E140L|SHV-E140S|SHV-E150S|SHV-E230K|SHV-E230L|SHV-E230S|SHW-M180K|SHW-M180L|SHW-M180S|SHW-M180W|SHW-M300W|SHW-M305W|SHW-M380K|SHW-M380S|SHW-M380W|SHW-M430W|SHW-M480K|SHW-M480S|SHW-M480W|SHW-M485W|SHW-M486W|SHW-M500W|GT-I9228|SCH-P739|SCH-I925|GT-I9200|GT-P5200|GT-P5210|GT-P5210X|SM-T311|SM-T310|SM-T310X|SM-T210|SM-T210R|SM-T211|SM-P600|SM-P601|SM-P605|SM-P900|SM-P901|SM-T217|SM-T217A|SM-T217S|SM-P6000|SM-T3100|SGH-I467|XE500|SM-T110|GT-P5220|GT-I9200X|GT-N5110X|GT-N5120|SM-P905|SM-T111|SM-T2105|SM-T315|SM-T320|SM-T320X|SM-T321|SM-T520|SM-T525|SM-T530NU|SM-T230NU|SM-T330NU|SM-T900|XE500T1C|SM-P605V|SM-P905V|SM-T337V|SM-T537V|SM-T707V|SM-T807V|SM-P600X|SM-P900X|SM-T210X|SM-T230|SM-T230X|SM-T325|GT-P7503|SM-T531|SM-T330|SM-T530|SM-T705|SM-T705C|SM-T535|SM-T331|SM-T800|SM-T700|SM-T537|SM-T807|SM-P907A|SM-T337A|SM-T537A|SM-T707A|SM-T807A|SM-T237|SM-T807P|SM-P607T|SM-T217T|SM-T337T|SM-T807T|SM-T116NQ|SM-P550|SM-T350|SM-T550|SM-T9000|SM-P9000|SM-T705Y|SM-T805|GT-P3113|SM-T710|SM-T810|SM-T815|SM-T360|SM-T533|SM-T113|SM-T335|SM-T715|SM-T560|SM-T670|SM-T677|SM-T377|SM-T567|SM-T357T|SM-T555|SM-T561",
    43.         "Kindle": "Kindle|Silk.*Accelerated|Android.*\\b(KFOT|KFTT|KFJWI|KFJWA|KFOTE|KFSOWI|KFTHWI|KFTHWA|KFAPWI|KFAPWA|WFJWAE|KFSAWA|KFSAWI|KFASWI|KFARWI)\\b",
    44.         "SurfaceTablet": "Windows NT [0-9.]+; ARM;.*(Tablet|ARMBJS)",
    45.         "HPTablet": "HP Slate (7|8|10)|HP ElitePad 900|hp-tablet|EliteBook.*Touch|HP 8|Slate 21|HP SlateBook 10",
    46.         "AsusTablet": "^.*PadFone((?!Mobile).)*$|Transformer|TF101|TF101G|TF300T|TF300TG|TF300TL|TF700T|TF700KL|TF701T|TF810C|ME171|ME301T|ME302C|ME371MG|ME370T|ME372MG|ME172V|ME173X|ME400C|Slider SL101|\\bK00F\\b|\\bK00C\\b|\\bK00E\\b|\\bK00L\\b|TX201LA|ME176C|ME102A|\\bM80TA\\b|ME372CL|ME560CG|ME372CG|ME302KL| K010 | K017 |ME572C|ME103K|ME170C|ME171C|\\bME70C\\b|ME581C|ME581CL|ME8510C|ME181C|P01Y|PO1MA",
    47.         "BlackBerryTablet": "PlayBook|RIM Tablet",
    48.         "HTCtablet": "HTC_Flyer_P512|HTC Flyer|HTC Jetstream|HTC-P715a|HTC EVO View 4G|PG41200|PG09410",
    49.         "MotorolaTablet": "xoom|sholest|MZ615|MZ605|MZ505|MZ601|MZ602|MZ603|MZ604|MZ606|MZ607|MZ608|MZ609|MZ615|MZ616|MZ617",
    50.         "NookTablet": "Android.*Nook|NookColor|nook browser|BNRV200|BNRV200A|BNTV250|BNTV250A|BNTV400|BNTV600|LogicPD Zoom2",
    51.         "AcerTablet": "Android.*; \\b(A100|A101|A110|A200|A210|A211|A500|A501|A510|A511|A700|A701|W500|W500P|W501|W501P|W510|W511|W700|G100|G100W|B1-A71|B1-710|B1-711|A1-810|A1-811|A1-830)\\b|W3-810|\\bA3-A10\\b|\\bA3-A11\\b|\\bA3-A20",
    52.         "ToshibaTablet": "Android.*(AT100|AT105|AT200|AT205|AT270|AT275|AT300|AT305|AT1S5|AT500|AT570|AT700|AT830)|TOSHIBA.*FOLIO",
    53.         "LGTablet": "\\bL-06C|LG-V909|LG-V900|LG-V700|LG-V510|LG-V500|LG-V410|LG-V400|LG-VK810\\b",
    54.         "FujitsuTablet": "Android.*\\b(F-01D|F-02F|F-05E|F-10D|M532|Q572)\\b",
    55.         "PrestigioTablet": "PMP3170B|PMP3270B|PMP3470B|PMP7170B|PMP3370B|PMP3570C|PMP5870C|PMP3670B|PMP5570C|PMP5770D|PMP3970B|PMP3870C|PMP5580C|PMP5880D|PMP5780D|PMP5588C|PMP7280C|PMP7280C3G|PMP7280|PMP7880D|PMP5597D|PMP5597|PMP7100D|PER3464|PER3274|PER3574|PER3884|PER5274|PER5474|PMP5097CPRO|PMP5097|PMP7380D|PMP5297C|PMP5297C_QUAD|PMP812E|PMP812E3G|PMP812F|PMP810E|PMP880TD|PMT3017|PMT3037|PMT3047|PMT3057|PMT7008|PMT5887|PMT5001|PMT5002",
    56.         "LenovoTablet": "Lenovo TAB|Idea(Tab|Pad)( A1|A10| K1|)|ThinkPad([ ]+)?Tablet|YT3-X90L|YT3-X90F|YT3-X90X|Lenovo.*(S2109|S2110|S5000|S6000|K3011|A3000|A3500|A1000|A2107|A2109|A1107|A5500|A7600|B6000|B8000|B8080)(-|)(FL|F|HV|H|)",
    57.         "DellTablet": "Venue 11|Venue 8|Venue 7|Dell Streak 10|Dell Streak 7",
    58.         "YarvikTablet": "Android.*\\b(TAB210|TAB211|TAB224|TAB250|TAB260|TAB264|TAB310|TAB360|TAB364|TAB410|TAB411|TAB420|TAB424|TAB450|TAB460|TAB461|TAB464|TAB465|TAB467|TAB468|TAB07-100|TAB07-101|TAB07-150|TAB07-151|TAB07-152|TAB07-200|TAB07-201-3G|TAB07-210|TAB07-211|TAB07-212|TAB07-214|TAB07-220|TAB07-400|TAB07-485|TAB08-150|TAB08-200|TAB08-201-3G|TAB08-201-30|TAB09-100|TAB09-211|TAB09-410|TAB10-150|TAB10-201|TAB10-211|TAB10-400|TAB10-410|TAB13-201|TAB274EUK|TAB275EUK|TAB374EUK|TAB462EUK|TAB474EUK|TAB9-200)\\b",
    59.         "MedionTablet": "Android.*\\bOYO\\b|LIFE.*(P9212|P9514|P9516|S9512)|LIFETAB",
    60.         "ArnovaTablet": "AN10G2|AN7bG3|AN7fG3|AN8G3|AN8cG3|AN7G3|AN9G3|AN7dG3|AN7dG3ST|AN7dG3ChildPad|AN10bG3|AN10bG3DT|AN9G2",
    61.         "IntensoTablet": "INM8002KP|INM1010FP|INM805ND|Intenso Tab|TAB1004",
    62.         "IRUTablet": "M702pro",
    63.         "MegafonTablet": "MegaFon V9|\\bZTE V9\\b|Android.*\\bMT7A\\b",
    64.         "EbodaTablet": "E-Boda (Supreme|Impresspeed|Izzycomm|Essential)",
    65.         "AllViewTablet": "Allview.*(Viva|Alldro|City|Speed|All TV|Frenzy|Quasar|Shine|TX1|AX1|AX2)",
    66.         "ArchosTablet": "\\b(101G9|80G9|A101IT)\\b|Qilive 97R|Archos5|\\bARCHOS (70|79|80|90|97|101|FAMILYPAD|)(b|)(G10| Cobalt| TITANIUM(HD|)| Xenon| Neon|XSK| 2| XS 2| PLATINUM| CARBON|GAMEPAD)\\b",
    67.         "AinolTablet": "NOVO7|NOVO8|NOVO10|Novo7Aurora|Novo7Basic|NOVO7PALADIN|novo9-Spark",
    68.         "NokiaLumiaTablet": "Lumia 2520",
    69.         "SonyTablet": "Sony.*Tablet|Xperia Tablet|Sony Tablet S|SO-03E|SGPT12|SGPT13|SGPT114|SGPT121|SGPT122|SGPT123|SGPT111|SGPT112|SGPT113|SGPT131|SGPT132|SGPT133|SGPT211|SGPT212|SGPT213|SGP311|SGP312|SGP321|EBRD1101|EBRD1102|EBRD1201|SGP351|SGP341|SGP511|SGP512|SGP521|SGP541|SGP551|SGP621|SGP612|SOT31",
    70.         "PhilipsTablet": "\\b(PI2010|PI3000|PI3100|PI3105|PI3110|PI3205|PI3210|PI3900|PI4010|PI7000|PI7100)\\b",
    71.         "CubeTablet": "Android.*(K8GT|U9GT|U10GT|U16GT|U17GT|U18GT|U19GT|U20GT|U23GT|U30GT)|CUBE U8GT",
    72.         "CobyTablet": "MID1042|MID1045|MID1125|MID1126|MID7012|MID7014|MID7015|MID7034|MID7035|MID7036|MID7042|MID7048|MID7127|MID8042|MID8048|MID8127|MID9042|MID9740|MID9742|MID7022|MID7010",
    73.         "MIDTablet": "M9701|M9000|M9100|M806|M1052|M806|T703|MID701|MID713|MID710|MID727|MID760|MID830|MID728|MID933|MID125|MID810|MID732|MID120|MID930|MID800|MID731|MID900|MID100|MID820|MID735|MID980|MID130|MID833|MID737|MID960|MID135|MID860|MID736|MID140|MID930|MID835|MID733|MID4X10",
    74.         "MSITablet": "MSI \\b(Primo 73K|Primo 73L|Primo 81L|Primo 77|Primo 93|Primo 75|Primo 76|Primo 73|Primo 81|Primo 91|Primo 90|Enjoy 71|Enjoy 7|Enjoy 10)\\b",
    75.         "SMiTTablet": "Android.*(\\bMID\\b|MID-560|MTV-T1200|MTV-PND531|MTV-P1101|MTV-PND530)",
    76.         "RockChipTablet": "Android.*(RK2818|RK2808A|RK2918|RK3066)|RK2738|RK2808A",
    77.         "FlyTablet": "IQ310|Fly Vision",
    78.         "bqTablet": "Android.*(bq)?.*(Elcano|Curie|Edison|Maxwell|Kepler|Pascal|Tesla|Hypatia|Platon|Newton|Livingstone|Cervantes|Avant|Aquaris E10)|Maxwell.*Lite|Maxwell.*Plus",
    79.         "HuaweiTablet": "MediaPad|MediaPad 7 Youth|IDEOS S7|S7-201c|S7-202u|S7-101|S7-103|S7-104|S7-105|S7-106|S7-201|S7-Slim",
    80.         "NecTablet": "\\bN-06D|\\bN-08D",
    81.         "PantechTablet": "Pantech.*P4100",
    82.         "BronchoTablet": "Broncho.*(N701|N708|N802|a710)",
    83.         "VersusTablet": "TOUCHPAD.*[78910]|\\bTOUCHTAB\\b",
    84.         "ZyncTablet": "z1000|Z99 2G|z99|z930|z999|z990|z909|Z919|z900",
    85.         "PositivoTablet": "TB07STA|TB10STA|TB07FTA|TB10FTA",
    86.         "NabiTablet": "Android.*\\bNabi",
    87.         "KoboTablet": "Kobo Touch|\\bK080\\b|\\bVox\\b Build|\\bArc\\b Build",
    88.         "DanewTablet": "DSlide.*\\b(700|701R|702|703R|704|802|970|971|972|973|974|1010|1012)\\b",
    89.         "TexetTablet": "NaviPad|TB-772A|TM-7045|TM-7055|TM-9750|TM-7016|TM-7024|TM-7026|TM-7041|TM-7043|TM-7047|TM-8041|TM-9741|TM-9747|TM-9748|TM-9751|TM-7022|TM-7021|TM-7020|TM-7011|TM-7010|TM-7023|TM-7025|TM-7037W|TM-7038W|TM-7027W|TM-9720|TM-9725|TM-9737W|TM-1020|TM-9738W|TM-9740|TM-9743W|TB-807A|TB-771A|TB-727A|TB-725A|TB-719A|TB-823A|TB-805A|TB-723A|TB-715A|TB-707A|TB-705A|TB-709A|TB-711A|TB-890HD|TB-880HD|TB-790HD|TB-780HD|TB-770HD|TB-721HD|TB-710HD|TB-434HD|TB-860HD|TB-840HD|TB-760HD|TB-750HD|TB-740HD|TB-730HD|TB-722HD|TB-720HD|TB-700HD|TB-500HD|TB-470HD|TB-431HD|TB-430HD|TB-506|TB-504|TB-446|TB-436|TB-416|TB-146SE|TB-126SE",
    90.         "PlaystationTablet": "Playstation.*(Portable|Vita)",
    91.         "TrekstorTablet": "ST10416-1|VT10416-1|ST70408-1|ST702xx-1|ST702xx-2|ST80208|ST97216|ST70104-2|VT10416-2|ST10216-2A|SurfTab",
    92.         "PyleAudioTablet": "\\b(PTBL10CEU|PTBL10C|PTBL72BC|PTBL72BCEU|PTBL7CEU|PTBL7C|PTBL92BC|PTBL92BCEU|PTBL9CEU|PTBL9CUK|PTBL9C)\\b",
    93.         "AdvanTablet": "Android.* \\b(E3A|T3X|T5C|T5B|T3E|T3C|T3B|T1J|T1F|T2A|T1H|T1i|E1C|T1-E|T5-A|T4|E1-B|T2Ci|T1-B|T1-D|O1-A|E1-A|T1-A|T3A|T4i)\\b ",
    94.         "DanyTechTablet": "Genius Tab G3|Genius Tab S2|Genius Tab Q3|Genius Tab G4|Genius Tab Q4|Genius Tab G-II|Genius TAB GII|Genius TAB GIII|Genius Tab S1",
    95.         "GalapadTablet": "Android.*\\bG1\\b",
    96.         "MicromaxTablet": "Funbook|Micromax.*\\b(P250|P560|P360|P362|P600|P300|P350|P500|P275)\\b",
    97.         "KarbonnTablet": "Android.*\\b(A39|A37|A34|ST8|ST10|ST7|Smart Tab3|Smart Tab2)\\b",
    98.         "AllFineTablet": "Fine7 Genius|Fine7 Shine|Fine7 Air|Fine8 Style|Fine9 More|Fine10 Joy|Fine11 Wide",
    99.         "PROSCANTablet": "\\b(PEM63|PLT1023G|PLT1041|PLT1044|PLT1044G|PLT1091|PLT4311|PLT4311PL|PLT4315|PLT7030|PLT7033|PLT7033D|PLT7035|PLT7035D|PLT7044K|PLT7045K|PLT7045KB|PLT7071KG|PLT7072|PLT7223G|PLT7225G|PLT7777G|PLT7810K|PLT7849G|PLT7851G|PLT7852G|PLT8015|PLT8031|PLT8034|PLT8036|PLT8080K|PLT8082|PLT8088|PLT8223G|PLT8234G|PLT8235G|PLT8816K|PLT9011|PLT9045K|PLT9233G|PLT9735|PLT9760G|PLT9770G)\\b",
    100.         "YONESTablet": "BQ1078|BC1003|BC1077|RK9702|BC9730|BC9001|IT9001|BC7008|BC7010|BC708|BC728|BC7012|BC7030|BC7027|BC7026",
    101.         "ChangJiaTablet": "TPC7102|TPC7103|TPC7105|TPC7106|TPC7107|TPC7201|TPC7203|TPC7205|TPC7210|TPC7708|TPC7709|TPC7712|TPC7110|TPC8101|TPC8103|TPC8105|TPC8106|TPC8203|TPC8205|TPC8503|TPC9106|TPC9701|TPC97101|TPC97103|TPC97105|TPC97106|TPC97111|TPC97113|TPC97203|TPC97603|TPC97809|TPC97205|TPC10101|TPC10103|TPC10106|TPC10111|TPC10203|TPC10205|TPC10503",
    102.         "GUTablet": "TX-A1301|TX-M9002|Q702|kf026",
    103.         "PointOfViewTablet": "TAB-P506|TAB-navi-7-3G-M|TAB-P517|TAB-P-527|TAB-P701|TAB-P703|TAB-P721|TAB-P731N|TAB-P741|TAB-P825|TAB-P905|TAB-P925|TAB-PR945|TAB-PL1015|TAB-P1025|TAB-PI1045|TAB-P1325|TAB-PROTAB[0-9]+|TAB-PROTAB25|TAB-PROTAB26|TAB-PROTAB27|TAB-PROTAB26XL|TAB-PROTAB2-IPS9|TAB-PROTAB30-IPS9|TAB-PROTAB25XXL|TAB-PROTAB26-IPS10|TAB-PROTAB30-IPS10",
    104.         "OvermaxTablet": "OV-(SteelCore|NewBase|Basecore|Baseone|Exellen|Quattor|EduTab|Solution|ACTION|BasicTab|TeddyTab|MagicTab|Stream|TB-08|TB-09)",
    105.         "HCLTablet": "HCL.*Tablet|Connect-3G-2.0|Connect-2G-2.0|ME Tablet U1|ME Tablet U2|ME Tablet G1|ME Tablet X1|ME Tablet Y2|ME Tablet Sync",
    106.         "DPSTablet": "DPS Dream 9|DPS Dual 7",
    107.         "VistureTablet": "V97 HD|i75 3G|Visture V4( HD)?|Visture V5( HD)?|Visture V10",
    108.         "CrestaTablet": "CTP(-)?810|CTP(-)?818|CTP(-)?828|CTP(-)?838|CTP(-)?888|CTP(-)?978|CTP(-)?980|CTP(-)?987|CTP(-)?988|CTP(-)?989",
    109.         "MediatekTablet": "\\bMT8125|MT8389|MT8135|MT8377\\b",
    110.         "ConcordeTablet": "Concorde([ ]+)?Tab|ConCorde ReadMan",
    111.         "GoCleverTablet": "GOCLEVER TAB|A7GOCLEVER|M1042|M7841|M742|R1042BK|R1041|TAB A975|TAB A7842|TAB A741|TAB A741L|TAB M723G|TAB M721|TAB A1021|TAB I921|TAB R721|TAB I720|TAB T76|TAB R70|TAB R76.2|TAB R106|TAB R83.2|TAB M813G|TAB I721|GCTA722|TAB I70|TAB I71|TAB S73|TAB R73|TAB R74|TAB R93|TAB R75|TAB R76.1|TAB A73|TAB A93|TAB A93.2|TAB T72|TAB R83|TAB R974|TAB R973|TAB A101|TAB A103|TAB A104|TAB A104.2|R105BK|M713G|A972BK|TAB A971|TAB R974.2|TAB R104|TAB R83.3|TAB A1042",
    112.         "ModecomTablet": "FreeTAB 9000|FreeTAB 7.4|FreeTAB 7004|FreeTAB 7800|FreeTAB 2096|FreeTAB 7.5|FreeTAB 1014|FreeTAB 1001 |FreeTAB 8001|FreeTAB 9706|FreeTAB 9702|FreeTAB 7003|FreeTAB 7002|FreeTAB 1002|FreeTAB 7801|FreeTAB 1331|FreeTAB 1004|FreeTAB 8002|FreeTAB 8014|FreeTAB 9704|FreeTAB 1003",
    113.         "VoninoTablet": "\\b(Argus[ _]?S|Diamond[ _]?79HD|Emerald[ _]?78E|Luna[ _]?70C|Onyx[ _]?S|Onyx[ _]?Z|Orin[ _]?HD|Orin[ _]?S|Otis[ _]?S|SpeedStar[ _]?S|Magnet[ _]?M9|Primus[ _]?94[ _]?3G|Primus[ _]?94HD|Primus[ _]?QS|Android.*\\bQ8\\b|Sirius[ _]?EVO[ _]?QS|Sirius[ _]?QS|Spirit[ _]?S)\\b",
    114.         "ECSTablet": "V07OT2|TM105A|S10OT1|TR10CS1",
    115.         "StorexTablet": "eZee[_']?(Tab|Go)[0-9]+|TabLC7|Looney Tunes Tab",
    116.         "VodafoneTablet": "SmartTab([ ]+)?[0-9]+|SmartTabII10|SmartTabII7|VF-1497",
    117.         "EssentielBTablet": "Smart[ ']?TAB[ ]+?[0-9]+|Family[ ']?TAB2",
    118.         "RossMoorTablet": "RM-790|RM-997|RMD-878G|RMD-974R|RMT-705A|RMT-701|RME-601|RMT-501|RMT-711",
    119.         "iMobileTablet": "i-mobile i-note",
    120.         "TolinoTablet": "tolino tab [0-9.]+|tolino shine",
    121.         "AudioSonicTablet": "\\bC-22Q|T7-QC|T-17B|T-17P\\b",
    122.         "AMPETablet": "Android.* A78 ",
    123.         "SkkTablet": "Android.* (SKYPAD|PHOENIX|CYCLOPS)",
    124.         "TecnoTablet": "TECNO P9",
    125.         "JXDTablet": "Android.* \\b(F3000|A3300|JXD5000|JXD3000|JXD2000|JXD300B|JXD300|S5800|S7800|S602b|S5110b|S7300|S5300|S602|S603|S5100|S5110|S601|S7100a|P3000F|P3000s|P101|P200s|P1000m|P200m|P9100|P1000s|S6600b|S908|P1000|P300|S18|S6600|S9100)\\b",
    126.         "iJoyTablet": "Tablet (Spirit 7|Essentia|Galatea|Fusion|Onix 7|Landa|Titan|Scooby|Deox|Stella|Themis|Argon|Unique 7|Sygnus|Hexen|Finity 7|Cream|Cream X2|Jade|Neon 7|Neron 7|Kandy|Scape|Saphyr 7|Rebel|Biox|Rebel|Rebel 8GB|Myst|Draco 7|Myst|Tab7-004|Myst|Tadeo Jones|Tablet Boing|Arrow|Draco Dual Cam|Aurix|Mint|Amity|Revolution|Finity 9|Neon 9|T9w|Amity 4GB Dual Cam|Stone 4GB|Stone 8GB|Andromeda|Silken|X2|Andromeda II|Halley|Flame|Saphyr 9,7|Touch 8|Planet|Triton|Unique 10|Hexen 10|Memphis 4GB|Memphis 8GB|Onix 10)",
    127.         "FX2Tablet": "FX2 PAD7|FX2 PAD10",
    128.         "XoroTablet": "KidsPAD 701|PAD[ ]?712|PAD[ ]?714|PAD[ ]?716|PAD[ ]?717|PAD[ ]?718|PAD[ ]?720|PAD[ ]?721|PAD[ ]?722|PAD[ ]?790|PAD[ ]?792|PAD[ ]?900|PAD[ ]?9715D|PAD[ ]?9716DR|PAD[ ]?9718DR|PAD[ ]?9719QR|PAD[ ]?9720QR|TelePAD1030|Telepad1032|TelePAD730|TelePAD731|TelePAD732|TelePAD735Q|TelePAD830|TelePAD9730|TelePAD795|MegaPAD 1331|MegaPAD 1851|MegaPAD 2151",
    129.         "ViewsonicTablet": "ViewPad 10pi|ViewPad 10e|ViewPad 10s|ViewPad E72|ViewPad7|ViewPad E100|ViewPad 7e|ViewSonic VB733|VB100a",
    130.         "OdysTablet": "LOOX|XENO10|ODYS[ -](Space|EVO|Xpress|NOON)|\\bXELIO\\b|Xelio10Pro|XELIO7PHONETAB|XELIO10EXTREME|XELIOPT2|NEO_QUAD10",
    131.         "CaptivaTablet": "CAPTIVA PAD",
    132.         "IconbitTablet": "NetTAB|NT-3702|NT-3702S|NT-3702S|NT-3603P|NT-3603P|NT-0704S|NT-0704S|NT-3805C|NT-3805C|NT-0806C|NT-0806C|NT-0909T|NT-0909T|NT-0907S|NT-0907S|NT-0902S|NT-0902S",
    133.         "TeclastTablet": "T98 4G|\\bP80\\b|\\bX90HD\\b|X98 Air|X98 Air 3G|\\bX89\\b|P80 3G|\\bX80h\\b|P98 Air|\\bX89HD\\b|P98 3G|\\bP90HD\\b|P89 3G|X98 3G|\\bP70h\\b|P79HD 3G|G18d 3G|\\bP79HD\\b|\\bP89s\\b|\\bA88\\b|\\bP10HD\\b|\\bP19HD\\b|G18 3G|\\bP78HD\\b|\\bA78\\b|\\bP75\\b|G17s 3G|G17h 3G|\\bP85t\\b|\\bP90\\b|\\bP11\\b|\\bP98t\\b|\\bP98HD\\b|\\bG18d\\b|\\bP85s\\b|\\bP11HD\\b|\\bP88s\\b|\\bA80HD\\b|\\bA80se\\b|\\bA10h\\b|\\bP89\\b|\\bP78s\\b|\\bG18\\b|\\bP85\\b|\\bA70h\\b|\\bA70\\b|\\bG17\\b|\\bP18\\b|\\bA80s\\b|\\bA11s\\b|\\bP88HD\\b|\\bA80h\\b|\\bP76s\\b|\\bP76h\\b|\\bP98\\b|\\bA10HD\\b|\\bP78\\b|\\bP88\\b|\\bA11\\b|\\bA10t\\b|\\bP76a\\b|\\bP76t\\b|\\bP76e\\b|\\bP85HD\\b|\\bP85a\\b|\\bP86\\b|\\bP75HD\\b|\\bP76v\\b|\\bA12\\b|\\bP75a\\b|\\bA15\\b|\\bP76Ti\\b|\\bP81HD\\b|\\bA10\\b|\\bT760VE\\b|\\bT720HD\\b|\\bP76\\b|\\bP73\\b|\\bP71\\b|\\bP72\\b|\\bT720SE\\b|\\bC520Ti\\b|\\bT760\\b|\\bT720VE\\b|T720-3GE|T720-WiFi",
    134.         "OndaTablet": "\\b(V975i|Vi30|VX530|V701|Vi60|V701s|Vi50|V801s|V719|Vx610w|VX610W|V819i|Vi10|VX580W|Vi10|V711s|V813|V811|V820w|V820|Vi20|V711|VI30W|V712|V891w|V972|V819w|V820w|Vi60|V820w|V711|V813s|V801|V819|V975s|V801|V819|V819|V818|V811|V712|V975m|V101w|V961w|V812|V818|V971|V971s|V919|V989|V116w|V102w|V973|Vi40)\\b[\\s]+",
    135.         "JaytechTablet": "TPC-PA762",
    136.         "BlaupunktTablet": "Endeavour 800NG|Endeavour 1010",
    137.         "DigmaTablet": "\\b(iDx10|iDx9|iDx8|iDx7|iDxD7|iDxD8|iDsQ8|iDsQ7|iDsQ8|iDsD10|iDnD7|3TS804H|iDsQ11|iDj7|iDs10)\\b",
    138.         "EvolioTablet": "ARIA_Mini_wifi|Aria[ _]Mini|Evolio X10|Evolio X7|Evolio X8|\\bEvotab\\b|\\bNeura\\b",
    139.         "LavaTablet": "QPAD E704|\\bIvoryS\\b|E-TAB IVORY|\\bE-TAB\\b",
    140.         "AocTablet": "MW0811|MW0812|MW0922|MTK8382|MW1031|MW0831|MW0821|MW0931|MW0712",
    141.         "MpmanTablet": "MP11 OCTA|MP10 OCTA|MPQC1114|MPQC1004|MPQC994|MPQC974|MPQC973|MPQC804|MPQC784|MPQC780|\\bMPG7\\b|MPDCG75|MPDCG71|MPDC1006|MP101DC|MPDC9000|MPDC905|MPDC706HD|MPDC706|MPDC705|MPDC110|MPDC100|MPDC99|MPDC97|MPDC88|MPDC8|MPDC77|MP709|MID701|MID711|MID170|MPDC703|MPQC1010",
    142.         "CelkonTablet": "CT695|CT888|CT[\\s]?910|CT7 Tab|CT9 Tab|CT3 Tab|CT2 Tab|CT1 Tab|C820|C720|\\bCT-1\\b",
    143.         "WolderTablet": "miTab \\b(DIAMOND|SPACE|BROOKLYN|NEO|FLY|MANHATTAN|FUNK|EVOLUTION|SKY|GOCAR|IRON|GENIUS|POP|MINT|EPSILON|BROADWAY|JUMP|HOP|LEGEND|NEW AGE|LINE|ADVANCE|FEEL|FOLLOW|LIKE|LINK|LIVE|THINK|FREEDOM|CHICAGO|CLEVELAND|BALTIMORE-GH|IOWA|BOSTON|SEATTLE|PHOENIX|DALLAS|IN 101|MasterChef)\\b",
    144.         "MiTablet": "\\bMI PAD\\b|\\bHM NOTE 1W\\b",
    145.         "NibiruTablet": "Nibiru M1|Nibiru Jupiter One",
    146.         "NexoTablet": "NEXO NOVA|NEXO 10|NEXO AVIO|NEXO FREE|NEXO GO|NEXO EVO|NEXO 3G|NEXO SMART|NEXO KIDDO|NEXO MOBI",
    147.         "LeaderTablet": "TBLT10Q|TBLT10I|TBL-10WDKB|TBL-10WDKBO2013|TBL-W230V2|TBL-W450|TBL-W500|SV572|TBLT7I|TBA-AC7-8G|TBLT79|TBL-8W16|TBL-10W32|TBL-10WKB|TBL-W100",
    148.         "UbislateTablet": "UbiSlate[\\s]?7C",
    149.         "PocketBookTablet": "Pocketbook",
    150.         "KocasoTablet": "\\b(TB-1207)\\b",
    151.         "Hudl": "Hudl HT7S3|Hudl 2",
    152.         "TelstraTablet": "T-Hub2",
    153.         "GenericTablet": "Android.*\\b97D\\b|Tablet(?!.*PC)|BNTV250A|MID-WCDMA|LogicPD Zoom2|\\bA7EB\\b|CatNova8|A1_07|CT704|CT1002|\\bM721\\b|rk30sdk|\\bEVOTAB\\b|M758A|ET904|ALUMIUM10|Smartfren Tab|Endeavour 1010|Tablet-PC-4|Tagi Tab|\\bM6pro\\b|CT1020W|arc 10HD|\\bJolla\\b|\\bTP750\\b"
    154.     },
    155.     "oss": {
    156.         "AndroidOS": "Android",
    157.         "BlackBerryOS": "blackberry|\\bBB10\\b|rim tablet os",
    158.         "PalmOS": "PalmOS|avantgo|blazer|elaine|hiptop|palm|plucker|xiino",
    159.         "SymbianOS": "Symbian|SymbOS|Series60|Series40|SYB-[0-9]+|\\bS60\\b",
    160.         "WindowsMobileOS": "Windows CE.*(PPC|Smartphone|Mobile|[0-9]{3}x[0-9]{3})|Window Mobile|Windows Phone [0-9.]+|WCE;",
    161.         "WindowsPhoneOS": "Windows Phone 10.0|Windows Phone 8.1|Windows Phone 8.0|Windows Phone OS|XBLWP7|ZuneWP7|Windows NT 6.[23]; ARM;",
    162.         "iOS": "\\biPhone.*Mobile|\\biPod|\\biPad",
    163.         "MeeGoOS": "MeeGo",
    164.         "MaemoOS": "Maemo",
    165.         "JavaOS": "J2ME\/|\\bMIDP\\b|\\bCLDC\\b",
    166.         "webOS": "webOS|hpwOS",
    167.         "badaOS": "\\bBada\\b",
    168.         "BREWOS": "BREW"
    169.     },
    170.     "uas": {
    171.         "Vivaldi": "Vivaldi",
    172.         "Chrome": "\\bCrMo\\b|CriOS|Android.*Chrome\/[.0-9]* (Mobile)?",
    173.         "Dolfin": "\\bDolfin\\b",
    174.         "Opera": "Opera.*Mini|Opera.*Mobi|Android.*Opera|Mobile.*OPR\/[0-9.]+|Coast\/[0-9.]+",
    175.         "Skyfire": "Skyfire",
    176.         "Edge": "Mobile Safari\/[.0-9]* Edge",
    177.         "IE": "IEMobile|MSIEMobile",
    178.         "Firefox": "fennec|firefox.*maemo|(Mobile|Tablet).*Firefox|Firefox.*Mobile",
    179.         "Bolt": "bolt",
    180.         "TeaShark": "teashark",
    181.         "Blazer": "Blazer",
    182.         "Safari": "Version.*Mobile.*Safari|Safari.*Mobile|MobileSafari",
    183.         "Tizen": "Tizen",
    184.         "UCBrowser": "UC.*Browser|UCWEB",
    185.         "baiduboxapp": "baiduboxapp",
    186.         "baidubrowser": "baidubrowser",
    187.         "DiigoBrowser": "DiigoBrowser",
    188.         "Puffin": "Puffin",
    189.         "Mercury": "\\bMercury\\b",
    190.         "ObigoBrowser": "Obigo",
    191.         "NetFront": "NF-Browser",
    192.         "GenericBrowser": "NokiaBrowser|OviBrowser|OneBrowser|TwonkyBeamBrowser|SEMC.*Browser|FlyFlow|Minimo|NetFront|Novarra-Vision|MQQBrowser|MicroMessenger",
    193.         "PaleMoon": "Android.*PaleMoon|Mobile.*PaleMoon"
    194.     },
    195.     "props": {
    196.         "Mobile": "Mobile\/[VER]",
    197.         "Build": "Build\/[VER]",
    198.         "Version": "Version\/[VER]",
    199.         "VendorID": "VendorID\/[VER]",
    200.         "iPad": "iPad.*CPU[a-z ]+[VER]",
    201.         "iPhone": "iPhone.*CPU[a-z ]+[VER]",
    202.         "iPod": "iPod.*CPU[a-z ]+[VER]",
    203.         "Kindle": "Kindle\/[VER]",
    204.         "Chrome": [
    205.             "Chrome\/[VER]",
    206.             "CriOS\/[VER]",
    207.             "CrMo\/[VER]"
    208.         ],
    209.         "Coast": [
    210.             "Coast\/[VER]"
    211.         ],
    212.         "Dolfin": "Dolfin\/[VER]",
    213.         "Firefox": "Firefox\/[VER]",
    214.         "Fennec": "Fennec\/[VER]",
    215.         "Edge": "Edge\/[VER]",
    216.         "IE": [
    217.             "IEMobile\/[VER];",
    218.             "IEMobile [VER]",
    219.             "MSIE [VER];",
    220.             "Trident\/[0-9.]+;.*rv:[VER]"
    221.         ],
    222.         "NetFront": "NetFront\/[VER]",
    223.         "NokiaBrowser": "NokiaBrowser\/[VER]",
    224.         "Opera": [
    225.             " OPR\/[VER]",
    226.             "Opera Mini\/[VER]",
    227.             "Version\/[VER]"
    228.         ],
    229.         "Opera Mini": "Opera Mini\/[VER]",
    230.         "Opera Mobi": "Version\/[VER]",
    231.         "UC Browser": "UC Browser[VER]",
    232.         "MQQBrowser": "MQQBrowser\/[VER]",
    233.         "MicroMessenger": "MicroMessenger\/[VER]",
    234.         "baiduboxapp": "baiduboxapp\/[VER]",
    235.         "baidubrowser": "baidubrowser\/[VER]",
    236.         "Iron": "Iron\/[VER]",
    237.         "Safari": [
    238.             "Version\/[VER]",
    239.             "Safari\/[VER]"
    240.         ],
    241.         "Skyfire": "Skyfire\/[VER]",
    242.         "Tizen": "Tizen\/[VER]",
    243.         "Webkit": "webkit[ \/][VER]",
    244.         "PaleMoon": "PaleMoon\/[VER]",
    245.         "Gecko": "Gecko\/[VER]",
    246.         "Trident": "Trident\/[VER]",
    247.         "Presto": "Presto\/[VER]",
    248.         "Goanna": "Goanna\/[VER]",
    249.         "iOS": " \\bi?OS\\b [VER][ ;]{1}",
    250.         "Android": "Android [VER]",
    251.         "BlackBerry": [
    252.             "BlackBerry[\\w]+\/[VER]",
    253.             "BlackBerry.*Version\/[VER]",
    254.             "Version\/[VER]"
    255.         ],
    256.         "BREW": "BREW [VER]",
    257.         "Java": "Java\/[VER]",
    258.         "Windows Phone OS": [
    259.             "Windows Phone OS [VER]",
    260.             "Windows Phone [VER]"
    261.         ],
    262.         "Windows Phone": "Windows Phone [VER]",
    263.         "Windows CE": "Windows CE\/[VER]",
    264.         "Windows NT": "Windows NT [VER]",
    265.         "Symbian": [
    266.             "SymbianOS\/[VER]",
    267.             "Symbian\/[VER]"
    268.         ],
    269.         "webOS": [
    270.             "webOS\/[VER]",
    271.             "hpwOS\/[VER];"
    272.         ]
    273.     },
    274.     "utils": {
    275.         "Bot": "Googlebot|facebookexternalhit|AdsBot-Google|Google Keyword Suggestion|Facebot|YandexBot|bingbot|ia_archiver|AhrefsBot|Ezooms|GSLFbot|WBSearchBot|Twitterbot|TweetmemeBot|Twikle|PaperLiBot|Wotbox|UnwindFetchor|Exabot|MJ12bot|YandexImages|TurnitinBot|Pingdom",
    276.         "MobileBot": "Googlebot-Mobile|AdsBot-Google-Mobile|YahooSeeker\/M1A1-R2D2",
    277.         "DesktopMode": "WPDesktop",
    278.         "TV": "SonyDTV|HbbTV",
    279.         "WebKit": "(webkit)[ \/]([\\w.]+)",
    280.         "Console": "\\b(Nintendo|Nintendo WiiU|Nintendo 3DS|PLAYSTATION|Xbox)\\b",
    281.         "Watch": "SM-V700"
    282.     }
    283. };
    284.  
    285.     // following patterns come from http://detectmobilebrowsers.com/
    286.     impl.detectMobileBrowsers = {
    287.         fullPattern: /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i,
    288.         shortPattern: /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i,
    289.         tabletPattern: /android|ipad|playbook|silk/i
    290.     };
    291.  
    292.     var hasOwnProp = Object.prototype.hasOwnProperty,
    293.         isArray;
    294.  
    295.     impl.FALLBACK_PHONE = 'UnknownPhone';
    296.     impl.FALLBACK_TABLET = 'UnknownTablet';
    297.     impl.FALLBACK_MOBILE = 'UnknownMobile';
    298.  
    299.     isArray = ('isArray' in Array) ?
    300.         Array.isArray : function (value) { return Object.prototype.toString.call(value) === '[object Array]'; };
    301.  
    302.     function equalIC(a, b) {
    303.         return a != null && b != null && a.toLowerCase() === b.toLowerCase();
    304.     }
    305.  
    306.     function containsIC(array, value) {
    307.         var valueLC, i, len = array.length;
    308.         if (!len || !value) {
    309.             return false;
    310.         }
    311.         valueLC = value.toLowerCase();
    312.         for (i = 0; i < len; ++i) {
    313.             if (valueLC === array[i].toLowerCase()) {
    314.                 return true;
    315.             }
    316.         }
    317.         return false;
    318.     }
    319.  
    320.     function convertPropsToRegExp(object) {
    321.         for (var key in object) {
    322.             if (hasOwnProp.call(object, key)) {
    323.                 object[key] = new RegExp(object[key], 'i');
    324.             }
    325.         }
    326.     }
    327.  
    328.     (function init() {
    329.         var key, values, value, i, len, verPos, mobileDetectRules = impl.mobileDetectRules;
    330.         for (key in mobileDetectRules.props) {
    331.             if (hasOwnProp.call(mobileDetectRules.props, key)) {
    332.                 values = mobileDetectRules.props[key];
    333.                 if (!isArray(values)) {
    334.                     values = [values];
    335.                 }
    336.                 len = values.length;
    337.                 for (i = 0; i < len; ++i) {
    338.                     value = values[i];
    339.                     verPos = value.indexOf('[VER]');
    340.                     if (verPos >= 0) {
    341.                         value = value.substring(0, verPos) + '([\\w._\\+]+)' + value.substring(verPos + 5);
    342.                     }
    343.                     values[i] = new RegExp(value, 'i');
    344.                 }
    345.                 mobileDetectRules.props[key] = values;
    346.             }
    347.         }
    348.         convertPropsToRegExp(mobileDetectRules.oss);
    349.         convertPropsToRegExp(mobileDetectRules.phones);
    350.         convertPropsToRegExp(mobileDetectRules.tablets);
    351.         convertPropsToRegExp(mobileDetectRules.uas);
    352.         convertPropsToRegExp(mobileDetectRules.utils);
    353.  
    354.         // copy some patterns to oss0 which are tested first (see issue#15)
    355.         mobileDetectRules.oss0 = {
    356.             WindowsPhoneOS: mobileDetectRules.oss.WindowsPhoneOS,
    357.             WindowsMobileOS: mobileDetectRules.oss.WindowsMobileOS
    358.         };
    359.     }());
    360.  
    361.     /**
    362.      * Test userAgent string against a set of rules and find the first matched key.
    363.      * @param {Object} rules (key is String, value is RegExp)
    364.      * @param {String} userAgent the navigator.userAgent (or HTTP-Header 'User-Agent').
    365.      * @returns {String|null} the matched key if found, otherwise <tt>null</tt>
    366.      * @private
    367.      */
    368.     impl.findMatch = function(rules, userAgent) {
    369.         for (var key in rules) {
    370.             if (hasOwnProp.call(rules, key)) {
    371.                 if (rules[key].test(userAgent)) {
    372.                     return key;
    373.                 }
    374.             }
    375.         }
    376.         return null;
    377.     };
    378.  
    379.     /**
    380.      * Test userAgent string against a set of rules and return an array of matched keys.
    381.      * @param {Object} rules (key is String, value is RegExp)
    382.      * @param {String} userAgent the navigator.userAgent (or HTTP-Header 'User-Agent').
    383.      * @returns {Array} an array of matched keys, may be empty when there is no match, but not <tt>null</tt>
    384.      * @private
    385.      */
    386.     impl.findMatches = function(rules, userAgent) {
    387.         var result = [];
    388.         for (var key in rules) {
    389.             if (hasOwnProp.call(rules, key)) {
    390.                 if (rules[key].test(userAgent)) {
    391.                     result.push(key);
    392.                 }
    393.             }
    394.         }
    395.         return result;
    396.     };
    397.  
    398.     /**
    399.      * Check the version of the given property in the User-Agent.
    400.      *
    401.      * @param {String} propertyName
    402.      * @param {String} userAgent
    403.      * @return {String} version or <tt>null</tt> if version not found
    404.      * @private
    405.      */
    406.     impl.getVersionStr = function (propertyName, userAgent) {
    407.         var props = impl.mobileDetectRules.props, patterns, i, len, match;
    408.         if (hasOwnProp.call(props, propertyName)) {
    409.             patterns = props[propertyName];
    410.             len = patterns.length;
    411.             for (i = 0; i < len; ++i) {
    412.                 match = patterns[i].exec(userAgent);
    413.                 if (match !== null) {
    414.                     return match[1];
    415.                 }
    416.             }
    417.         }
    418.         return null;
    419.     };
    420.  
    421.     /**
    422.      * Check the version of the given property in the User-Agent.
    423.      * Will return a float number. (eg. 2_0 will return 2.0, 4.3.1 will return 4.31)
    424.      *
    425.      * @param {String} propertyName
    426.      * @param {String} userAgent
    427.      * @return {Number} version or <tt>NaN</tt> if version not found
    428.      * @private
    429.      */
    430.     impl.getVersion = function (propertyName, userAgent) {
    431.         var version = impl.getVersionStr(propertyName, userAgent);
    432.         return version ? impl.prepareVersionNo(version) : NaN;
    433.     };
    434.  
    435.     /**
    436.      * Prepare the version number.
    437.      *
    438.      * @param {String} version
    439.      * @return {Number} the version number as a floating number
    440.      * @private
    441.      */
    442.     impl.prepareVersionNo = function (version) {
    443.         var numbers;
    444.  
    445.         numbers = version.split(/[a-z._ \/\-]/i);
    446.         if (numbers.length === 1) {
    447.             version = numbers[0];
    448.         }
    449.         if (numbers.length > 1) {
    450.             version = numbers[0] + '.';
    451.             numbers.shift();
    452.             version += numbers.join('');
    453.         }
    454.         return Number(version);
    455.     };
    456.  
    457.     impl.isMobileFallback = function (userAgent) {
    458.         return impl.detectMobileBrowsers.fullPattern.test(userAgent) ||
    459.             impl.detectMobileBrowsers.shortPattern.test(userAgent.substr(0,4));
    460.     };
    461.  
    462.     impl.isTabletFallback = function (userAgent) {
    463.         return impl.detectMobileBrowsers.tabletPattern.test(userAgent);
    464.     };
    465.  
    466.     impl.prepareDetectionCache = function (cache, userAgent, maxPhoneWidth) {
    467.         if (cache.mobile !== undefined) {
    468.             return;
    469.         }
    470.         var phone, tablet, phoneSized;
    471.  
    472.         // first check for stronger tablet rules, then phone (see issue#5)
    473.         tablet = impl.findMatch(impl.mobileDetectRules.tablets, userAgent);
    474.         if (tablet) {
    475.             cache.mobile = cache.tablet = tablet;
    476.             cache.phone = null;
    477.             return; // unambiguously identified as tablet
    478.         }
    479.  
    480.         phone = impl.findMatch(impl.mobileDetectRules.phones, userAgent);
    481.         if (phone) {
    482.             cache.mobile = cache.phone = phone;
    483.             cache.tablet = null;
    484.             return; // unambiguously identified as phone
    485.         }
    486.  
    487.         // our rules haven't found a match -> try more general fallback rules
    488.         if (impl.isMobileFallback(userAgent)) {
    489.             phoneSized = MobileDetect.isPhoneSized(maxPhoneWidth);
    490.             if (phoneSized === undefined) {
    491.                 cache.mobile = impl.FALLBACK_MOBILE;
    492.                 cache.tablet = cache.phone = null;
    493.             } else if (phoneSized) {
    494.                 cache.mobile = cache.phone = impl.FALLBACK_PHONE;
    495.                 cache.tablet = null;
    496.             } else {
    497.                 cache.mobile = cache.tablet = impl.FALLBACK_TABLET;
    498.                 cache.phone = null;
    499.             }
    500.         } else if (impl.isTabletFallback(userAgent)) {
    501.             cache.mobile = cache.tablet = impl.FALLBACK_TABLET;
    502.             cache.phone = null;
    503.         } else {
    504.             // not mobile at all!
    505.             cache.mobile = cache.tablet = cache.phone = null;
    506.         }
    507.     };
    508.  
    509.     // t is a reference to a MobileDetect instance
    510.     impl.mobileGrade = function (t) {
    511.         // impl note:
    512.         // To keep in sync w/ Mobile_Detect.php easily, the following code is tightly aligned to the PHP version.
    513.         // When changes are made in Mobile_Detect.php, copy this method and replace:
    514.         //     $this-> / t.
    515.         //     self::MOBILE_GRADE_(.) / '$1'
    516.         //     , self::VERSION_TYPE_FLOAT / (nothing)
    517.         //     isIOS() / os('iOS')
    518.         //     [reg] / (nothing)   <-- jsdelivr complaining about unescaped unicode character U+00AE
    519.         var $isMobile = t.mobile() !== null;
    520.  
    521.         if (
    522.             // Apple iOS 3.2-5.1 - Tested on the original iPad (4.3 / 5.0), iPad 2 (4.3), iPad 3 (5.1), original iPhone (3.1), iPhone 3 (3.2), 3GS (4.3), 4 (4.3 / 5.0), and 4S (5.1)
    523.             t.os('iOS') && t.version('iPad')>=4.3 ||
    524.             t.os('iOS') && t.version('iPhone')>=3.1 ||
    525.             t.os('iOS') && t.version('iPod')>=3.1 ||
    526.  
    527.             // Android 2.1-2.3 - Tested on the HTC Incredible (2.2), original Droid (2.2), HTC Aria (2.1), Google Nexus S (2.3). Functional on 1.5 & 1.6 but performance may be sluggish, tested on Google G1 (1.5)
    528.             // Android 3.1 (Honeycomb)  - Tested on the Samsung Galaxy Tab 10.1 and Motorola XOOM
    529.             // Android 4.0 (ICS)  - Tested on a Galaxy Nexus. Note: transition performance can be poor on upgraded devices
    530.             // Android 4.1 (Jelly Bean)  - Tested on a Galaxy Nexus and Galaxy 7
    531.             ( t.version('Android')>2.1 && t.is('Webkit') ) ||
    532.  
    533.             // Windows Phone 7-7.5 - Tested on the HTC Surround (7.0) HTC Trophy (7.5), LG-E900 (7.5), Nokia Lumia 800
    534.             t.version('Windows Phone OS')>=7.0 ||
    535.  
    536.             // Blackberry 7 - Tested on BlackBerry Torch 9810
    537.             // Blackberry 6.0 - Tested on the Torch 9800 and Style 9670
    538.             t.is('BlackBerry') && t.version('BlackBerry')>=6.0 ||
    539.             // Blackberry Playbook (1.0-2.0) - Tested on PlayBook
    540.             t.match('Playbook.*Tablet') ||
    541.  
    542.             // Palm WebOS (1.4-2.0) - Tested on the Palm Pixi (1.4), Pre (1.4), Pre 2 (2.0)
    543.             ( t.version('webOS')>=1.4 && t.match('Palm|Pre|Pixi') ) ||
    544.             // Palm WebOS 3.0  - Tested on HP TouchPad
    545.             t.match('hp.*TouchPad') ||
    546.  
    547.             // Firefox Mobile (12 Beta) - Tested on Android 2.3 device
    548.             ( t.is('Firefox') && t.version('Firefox')>=12 ) ||
    549.  
    550.             // Chrome for Android - Tested on Android 4.0, 4.1 device
    551.             ( t.is('Chrome') && t.is('AndroidOS') && t.version('Android')>=4.0 ) ||
    552.  
    553.             // Skyfire 4.1 - Tested on Android 2.3 device
    554.             ( t.is('Skyfire') && t.version('Skyfire')>=4.1 && t.is('AndroidOS') && t.version('Android')>=2.3 ) ||
    555.  
    556.             // Opera Mobile 11.5-12: Tested on Android 2.3
    557.             ( t.is('Opera') && t.version('Opera Mobi')>11 && t.is('AndroidOS') ) ||
    558.  
    559.             // Meego 1.2 - Tested on Nokia 950 and N9
    560.             t.is('MeeGoOS') ||
    561.  
    562.             // Tizen (pre-release) - Tested on early hardware
    563.             t.is('Tizen') ||
    564.  
    565.             // Samsung Bada 2.0 - Tested on a Samsung Wave 3, Dolphin browser
    566.             // @todo: more tests here!
    567.             t.is('Dolfin') && t.version('Bada')>=2.0 ||
    568.  
    569.             // UC Browser - Tested on Android 2.3 device
    570.             ( (t.is('UC Browser') || t.is('Dolfin')) && t.version('Android')>=2.3 ) ||
    571.  
    572.             // Kindle 3 and Fire  - Tested on the built-in WebKit browser for each
    573.             ( t.match('Kindle Fire') ||
    574.                 t.is('Kindle') && t.version('Kindle')>=3.0 ) ||
    575.  
    576.             // Nook Color 1.4.1 - Tested on original Nook Color, not Nook Tablet
    577.             t.is('AndroidOS') && t.is('NookTablet') ||
    578.  
    579.             // Chrome Desktop 11-21 - Tested on OS X 10.7 and Windows 7
    580.             t.version('Chrome')>=11 && !$isMobile ||
    581.  
    582.             // Safari Desktop 4-5 - Tested on OS X 10.7 and Windows 7
    583.             t.version('Safari')>=5.0 && !$isMobile ||
    584.  
    585.             // Firefox Desktop 4-13 - Tested on OS X 10.7 and Windows 7
    586.             t.version('Firefox')>=4.0 && !$isMobile ||
    587.  
    588.             // Internet Explorer 7-9 - Tested on Windows XP, Vista and 7
    589.             t.version('MSIE')>=7.0 && !$isMobile ||
    590.  
    591.             // Opera Desktop 10-12 - Tested on OS X 10.7 and Windows 7
    592.             // @reference: http://my.opera.com/community/openweb/idopera/
    593.             t.version('Opera')>=10 && !$isMobile
    594.  
    595.             ){
    596.             return 'A';
    597.         }
    598.  
    599.         if (
    600.             t.os('iOS') && t.version('iPad')<4.3 ||
    601.             t.os('iOS') && t.version('iPhone')<3.1 ||
    602.             t.os('iOS') && t.version('iPod')<3.1 ||
    603.  
    604.             // Blackberry 5.0: Tested on the Storm 2 9550, Bold 9770
    605.             t.is('Blackberry') && t.version('BlackBerry')>=5 && t.version('BlackBerry')<6 ||
    606.  
    607.             //Opera Mini (5.0-6.5) - Tested on iOS 3.2/4.3 and Android 2.3
    608.             ( t.version('Opera Mini')>=5.0 && t.version('Opera Mini')<=6.5 &&
    609.                 (t.version('Android')>=2.3 || t.is('iOS')) ) ||
    610.  
    611.             // Nokia Symbian^3 - Tested on Nokia N8 (Symbian^3), C7 (Symbian^3), also works on N97 (Symbian^1)
    612.             t.match('NokiaN8|NokiaC7|N97.*Series60|Symbian/3') ||
    613.  
    614.             // @todo: report this (tested on Nokia N71)
    615.             t.version('Opera Mobi')>=11 && t.is('SymbianOS')
    616.             ){
    617.             return 'B';
    618.         }
    619.  
    620.         if (
    621.         // Blackberry 4.x - Tested on the Curve 8330
    622.             t.version('BlackBerry')<5.0 ||
    623.             // Windows Mobile - Tested on the HTC Leo (WinMo 5.2)
    624.             t.match('MSIEMobile|Windows CE.*Mobile') || t.version('Windows Mobile')<=5.2
    625.  
    626.             ){
    627.             return 'C';
    628.         }
    629.  
    630.         //All older smartphone platforms and featurephones - Any device that doesn't support media queries
    631.         //will receive the basic, C grade experience.
    632.         return 'C';
    633.     };
    634.  
    635.     impl.detectOS = function (ua) {
    636.         return impl.findMatch(impl.mobileDetectRules.oss0, ua) ||
    637.             impl.findMatch(impl.mobileDetectRules.oss, ua);
    638.     };
    639.  
    640.     impl.getDeviceSmallerSide = function () {
    641.         return window.screen.width < window.screen.height ?
    642.             window.screen.width :
    643.             window.screen.height;
    644.     };
    645.  
    646.     /**
    647.      * Constructor for MobileDetect object.
    648.      * <br>
    649.      * Such an object will keep a reference to the given user-agent string and cache most of the detect queries.<br>
    650.      * <div style="background-color: #d9edf7; border: 1px solid #bce8f1; color: #3a87ad; padding: 14px; border-radius: 2px; margin-top: 20px">
    651.      *     <strong>Find information how to download and install:</strong>
    652.      *     <a href="https://github.com/hgoebl/mobile-detect.js/">github.com/hgoebl/mobile-detect.js/</a>
    653.      * </div>
    654.      *
    655.      * @example <pre>
    656.      *     var md = new MobileDetect(window.navigator.userAgent);
    657.      *     if (md.mobile()) {
    658.      *         location.href = (md.mobileGrade() === 'A') ? '/mobile/' : '/lynx/';
    659.      *     }
    660.      * </pre>
    661.      *
    662.      * @param {string} userAgent typically taken from window.navigator.userAgent or http_header['User-Agent']
    663.      * @param {number} [maxPhoneWidth=600] <strong>only for browsers</strong> specify a value for the maximum
    664.      *        width of smallest device side (in logical "CSS" pixels) until a device detected as mobile will be handled
    665.      *        as phone.
    666.      *        This is only used in cases where the device cannot be classified as phone or tablet.<br>
    667.      *        See <a href="http://developer.android.com/guide/practices/screens_support.html">Declaring Tablet Layouts
    668.      *        for Android</a>.<br>
    669.      *        If you provide a value < 0, then this "fuzzy" check is disabled.
    670.      * @constructor
    671.      * @global
    672.      */
    673.     function MobileDetect(userAgent, maxPhoneWidth) {
    674.         this.ua = userAgent || '';
    675.         this._cache = {};
    676.         //600dp is typical 7" tablet minimum width
    677.         this.maxPhoneWidth = maxPhoneWidth || 600;
    678.     }
    679.  
    680.     MobileDetect.prototype = {
    681.         constructor: MobileDetect,
    682.  
    683.         /**
    684.          * Returns the detected phone or tablet type or <tt>null</tt> if it is not a mobile device.
    685.          * <br>
    686.          * For a list of possible return values see {@link MobileDetect#phone} and {@link MobileDetect#tablet}.<br>
    687.          * <br>
    688.          * If the device is not detected by the regular expressions from Mobile-Detect, a test is made against
    689.          * the patterns of <a href="http://detectmobilebrowsers.com/">detectmobilebrowsers.com</a>. If this test
    690.          * is positive, a value of <code>UnknownPhone</code>, <code>UnknownTablet</code> or
    691.          * <code>UnknownMobile</code> is returned.<br>
    692.          * When used in browser, the decision whether phone or tablet is made based on <code>screen.width/height</code>.<br>
    693.          * <br>
    694.          * When used server-side (node.js), there is no way to tell the difference between <code>UnknownTablet</code>
    695.          * and <code>UnknownMobile</code>, so you will get <code>UnknownMobile</code> here.<br>
    696.          * Be aware that since v1.0.0 in this special case you will get <code>UnknownMobile</code> only for:
    697.          * {@link MobileDetect#mobile}, not for {@link MobileDetect#phone} and {@link MobileDetect#tablet}.
    698.          * In versions before v1.0.0 all 3 methods returned <code>UnknownMobile</code> which was tedious to use.
    699.          * <br>
    700.          * In most cases you will use the return value just as a boolean.
    701.          *
    702.          * @returns {String} the key for the phone family or tablet family, e.g. "Nexus".
    703.          * @function MobileDetect#mobile
    704.          */
    705.         mobile: function () {
    706.             impl.prepareDetectionCache(this._cache, this.ua, this.maxPhoneWidth);
    707.             return this._cache.mobile;
    708.         },
    709.  
    710.         /**
    711.          * Returns the detected phone type/family string or <tt>null</tt>.
    712.          * <br>
    713.          * The returned tablet (family or producer) is one of following keys:<br>
    714.          * <br><tt>iPhone, BlackBerry, HTC, Nexus, Dell, Motorola, Samsung, LG, Sony, Asus,
    715.          * NokiaLumia, Micromax, Palm, Vertu, Pantech, Fly, Wiko, iMobile, SimValley,
    716.          * Wolfgang, Alcatel, Nintendo, Amoi, INQ, GenericPhone</tt><br>
    717.          * <br>
    718.          * If the device is not detected by the regular expressions from Mobile-Detect, a test is made against
    719.          * the patterns of <a href="http://detectmobilebrowsers.com/">detectmobilebrowsers.com</a>. If this test
    720.          * is positive, a value of <code>UnknownPhone</code> or <code>UnknownMobile</code> is returned.<br>
    721.          * When used in browser, the decision whether phone or tablet is made based on <code>screen.width/height</code>.<br>
    722.          * <br>
    723.          * When used server-side (node.js), there is no way to tell the difference between <code>UnknownTablet</code>
    724.          * and <code>UnknownMobile</code>, so you will get <code>null</code> here, while {@link MobileDetect#mobile}
    725.          * will return <code>UnknownMobile</code>.<br>
    726.          * Be aware that since v1.0.0 in this special case you will get <code>UnknownMobile</code> only for:
    727.          * {@link MobileDetect#mobile}, not for {@link MobileDetect#phone} and {@link MobileDetect#tablet}.
    728.          * In versions before v1.0.0 all 3 methods returned <code>UnknownMobile</code> which was tedious to use.
    729.          * <br>
    730.          * In most cases you will use the return value just as a boolean.
    731.          *
    732.          * @returns {String} the key of the phone family or producer, e.g. "iPhone"
    733.          * @function MobileDetect#phone
    734.          */
    735.         phone: function () {
    736.             impl.prepareDetectionCache(this._cache, this.ua, this.maxPhoneWidth);
    737.             return this._cache.phone;
    738.         },
    739.  
    740.         /**
    741.          * Returns the detected tablet type/family string or <tt>null</tt>.
    742.          * <br>
    743.          * The returned tablet (family or producer) is one of following keys:<br>
    744.          * <br><tt>iPad, NexusTablet, SamsungTablet, Kindle, SurfaceTablet, HPTablet, AsusTablet,
    745.          * BlackBerryTablet, HTCtablet, MotorolaTablet, NookTablet, AcerTablet,
    746.          * ToshibaTablet, LGTablet, FujitsuTablet, PrestigioTablet, LenovoTablet,
    747.          * DellTablet, YarvikTablet, MedionTablet, ArnovaTablet, IntensoTablet, IRUTablet,
    748.          * MegafonTablet, EbodaTablet, AllViewTablet, ArchosTablet, AinolTablet,
    749.          * NokiaLumiaTablet, SonyTablet, PhilipsTablet, CubeTablet, CobyTablet, MIDTablet,
    750.          * MSITablet, SMiTTablet, RockChipTablet, FlyTablet, bqTablet, HuaweiTablet,
    751.          * NecTablet, PantechTablet, BronchoTablet, VersusTablet, ZyncTablet,
    752.          * PositivoTablet, NabiTablet, KoboTablet, DanewTablet, TexetTablet,
    753.          * PlaystationTablet, TrekstorTablet, PyleAudioTablet, AdvanTablet,
    754.          * DanyTechTablet, GalapadTablet, MicromaxTablet, KarbonnTablet, AllFineTablet,
    755.          * PROSCANTablet, YONESTablet, ChangJiaTablet, GUTablet, PointOfViewTablet,
    756.          * OvermaxTablet, HCLTablet, DPSTablet, VistureTablet, CrestaTablet,
    757.          * MediatekTablet, ConcordeTablet, GoCleverTablet, ModecomTablet, VoninoTablet,
    758.          * ECSTablet, StorexTablet, VodafoneTablet, EssentielBTablet, RossMoorTablet,
    759.          * iMobileTablet, TolinoTablet, AudioSonicTablet, AMPETablet, SkkTablet,
    760.          * TecnoTablet, JXDTablet, iJoyTablet, FX2Tablet, XoroTablet, ViewsonicTablet,
    761.          * OdysTablet, CaptivaTablet, IconbitTablet, TeclastTablet, OndaTablet,
    762.          * JaytechTablet, BlaupunktTablet, DigmaTablet, EvolioTablet, LavaTablet,
    763.          * AocTablet, MpmanTablet, CelkonTablet, WolderTablet, MiTablet, NibiruTablet,
    764.          * NexoTablet, LeaderTablet, UbislateTablet, PocketBookTablet, KocasoTablet, Hudl,
    765.          * TelstraTablet, GenericTablet</tt><br>
    766.          * <br>
    767.          * If the device is not detected by the regular expressions from Mobile-Detect, a test is made against
    768.          * the patterns of <a href="http://detectmobilebrowsers.com/">detectmobilebrowsers.com</a>. If this test
    769.          * is positive, a value of <code>UnknownTablet</code> or <code>UnknownMobile</code> is returned.<br>
    770.          * When used in browser, the decision whether phone or tablet is made based on <code>screen.width/height</code>.<br>
    771.          * <br>
    772.          * When used server-side (node.js), there is no way to tell the difference between <code>UnknownTablet</code>
    773.          * and <code>UnknownMobile</code>, so you will get <code>null</code> here, while {@link MobileDetect#mobile}
    774.          * will return <code>UnknownMobile</code>.<br>
    775.          * Be aware that since v1.0.0 in this special case you will get <code>UnknownMobile</code> only for:
    776.          * {@link MobileDetect#mobile}, not for {@link MobileDetect#phone} and {@link MobileDetect#tablet}.
    777.          * In versions before v1.0.0 all 3 methods returned <code>UnknownMobile</code> which was tedious to use.
    778.          * <br>
    779.          * In most cases you will use the return value just as a boolean.
    780.          *
    781.          * @returns {String} the key of the tablet family or producer, e.g. "SamsungTablet"
    782.          * @function MobileDetect#tablet
    783.          */
    784.         tablet: function () {
    785.             impl.prepareDetectionCache(this._cache, this.ua, this.maxPhoneWidth);
    786.             return this._cache.tablet;
    787.         },
    788.  
    789.         /**
    790.          * Returns the (first) detected user-agent string or <tt>null</tt>.
    791.          * <br>
    792.          * The returned user-agent is one of following keys:<br>
    793.          * <br><tt>Vivaldi, Chrome, Dolfin, Opera, Skyfire, Edge, IE, Firefox, Bolt, TeaShark,
    794.          * Blazer, Safari, Tizen, UCBrowser, baiduboxapp, baidubrowser, DiigoBrowser,
    795.          * Puffin, Mercury, ObigoBrowser, NetFront, GenericBrowser, PaleMoon</tt><br>
    796.          * <br>
    797.          * In most cases calling {@link MobileDetect#userAgent} will be sufficient. But there are rare
    798.          * cases where a mobile device pretends to be more than one particular browser. You can get the
    799.          * list of all matches with {@link MobileDetect#userAgents} or check for a particular value by
    800.          * providing one of the defined keys as first argument to {@link MobileDetect#is}.
    801.          *
    802.          * @returns {String} the key for the detected user-agent or <tt>null</tt>
    803.          * @function MobileDetect#userAgent
    804.          */
    805.         userAgent: function () {
    806.             if (this._cache.userAgent === undefined) {
    807.                 this._cache.userAgent = impl.findMatch(impl.mobileDetectRules.uas, this.ua);
    808.             }
    809.             return this._cache.userAgent;
    810.         },
    811.  
    812.         /**
    813.          * Returns all detected user-agent strings.
    814.          * <br>
    815.          * The array is empty or contains one or more of following keys:<br>
    816.          * <br><tt>Vivaldi, Chrome, Dolfin, Opera, Skyfire, Edge, IE, Firefox, Bolt, TeaShark,
    817.          * Blazer, Safari, Tizen, UCBrowser, baiduboxapp, baidubrowser, DiigoBrowser,
    818.          * Puffin, Mercury, ObigoBrowser, NetFront, GenericBrowser, PaleMoon</tt><br>
    819.          * <br>
    820.          * In most cases calling {@link MobileDetect#userAgent} will be sufficient. But there are rare
    821.          * cases where a mobile device pretends to be more than one particular browser. You can get the
    822.          * list of all matches with {@link MobileDetect#userAgents} or check for a particular value by
    823.          * providing one of the defined keys as first argument to {@link MobileDetect#is}.
    824.          *
    825.          * @returns {Array} the array of detected user-agent keys or <tt>[]</tt>
    826.          * @function MobileDetect#userAgents
    827.          */
    828.         userAgents: function () {
    829.             if (this._cache.userAgents === undefined) {
    830.                 this._cache.userAgents = impl.findMatches(impl.mobileDetectRules.uas, this.ua);
    831.             }
    832.             return this._cache.userAgents;
    833.         },
    834.  
    835.         /**
    836.          * Returns the detected operating system string or <tt>null</tt>.
    837.          * <br>
    838.          * The operating system is one of following keys:<br>
    839.          * <br><tt>AndroidOS, BlackBerryOS, PalmOS, SymbianOS, WindowsMobileOS, WindowsPhoneOS,
    840.          * iOS, MeeGoOS, MaemoOS, JavaOS, webOS, badaOS, BREWOS</tt><br>
    841.          *
    842.          * @returns {String} the key for the detected operating system.
    843.          * @function MobileDetect#os
    844.          */
    845.         os: function () {
    846.             if (this._cache.os === undefined) {
    847.                 this._cache.os = impl.detectOS(this.ua);
    848.             }
    849.             return this._cache.os;
    850.         },
    851.  
    852.         /**
    853.          * Get the version (as Number) of the given property in the User-Agent.
    854.          * <br>
    855.          * Will return a float number. (eg. 2_0 will return 2.0, 4.3.1 will return 4.31)
    856.          *
    857.          * @param {String} key a key defining a thing which has a version.<br>
    858.          *        You can use one of following keys:<br>
    859.          * <br><tt>Mobile, Build, Version, VendorID, iPad, iPhone, iPod, Kindle, Chrome, Coast,
    860.          * Dolfin, Firefox, Fennec, Edge, IE, NetFront, NokiaBrowser, Opera, Opera Mini,
    861.          * Opera Mobi, UC Browser, MQQBrowser, MicroMessenger, baiduboxapp, baidubrowser,
    862.          * Iron, Safari, Skyfire, Tizen, Webkit, PaleMoon, Gecko, Trident, Presto, Goanna,
    863.          * iOS, Android, BlackBerry, BREW, Java, Windows Phone OS, Windows Phone, Windows
    864.          * CE, Windows NT, Symbian, webOS</tt><br>
    865.          *
    866.          * @returns {Number} the version as float or <tt>NaN</tt> if User-Agent doesn't contain this version.
    867.          *          Be careful when comparing this value with '==' operator!
    868.          * @function MobileDetect#version
    869.          */
    870.         version: function (key) {
    871.             return impl.getVersion(key, this.ua);
    872.         },
    873.  
    874.         /**
    875.          * Get the version (as String) of the given property in the User-Agent.
    876.          * <br>
    877.          *
    878.          * @param {String} key a key defining a thing which has a version.<br>
    879.          *        You can use one of following keys:<br>
    880.          * <br><tt>Mobile, Build, Version, VendorID, iPad, iPhone, iPod, Kindle, Chrome, Coast,
    881.          * Dolfin, Firefox, Fennec, Edge, IE, NetFront, NokiaBrowser, Opera, Opera Mini,
    882.          * Opera Mobi, UC Browser, MQQBrowser, MicroMessenger, baiduboxapp, baidubrowser,
    883.          * Iron, Safari, Skyfire, Tizen, Webkit, PaleMoon, Gecko, Trident, Presto, Goanna,
    884.          * iOS, Android, BlackBerry, BREW, Java, Windows Phone OS, Windows Phone, Windows
    885.          * CE, Windows NT, Symbian, webOS</tt><br>
    886.          *
    887.          * @returns {String} the "raw" version as String or <tt>null</tt> if User-Agent doesn't contain this version.
    888.          *
    889.          * @function MobileDetect#versionStr
    890.          */
    891.         versionStr: function (key) {
    892.             return impl.getVersionStr(key, this.ua);
    893.         },
    894.  
    895.         /**
    896.          * Global test key against userAgent, os, phone, tablet and some other properties of userAgent string.
    897.          *
    898.          * @param {String} key the key (case-insensitive) of a userAgent, an operating system, phone or
    899.          *        tablet family.<br>
    900.          *        For a complete list of possible values, see {@link MobileDetect#userAgent},
    901.          *        {@link MobileDetect#os}, {@link MobileDetect#phone}, {@link MobileDetect#tablet}.<br>
    902.          *        Additionally you have following keys:<br>
    903.          * <br><tt>Bot, MobileBot, DesktopMode, TV, WebKit, Console, Watch</tt><br>
    904.          *
    905.          * @returns {boolean} <tt>true</tt> when the given key is one of the defined keys of userAgent, os, phone,
    906.          *                    tablet or one of the listed additional keys, otherwise <tt>false</tt>
    907.          * @function MobileDetect#is
    908.          */
    909.         is: function (key) {
    910.             return containsIC(this.userAgents(), key) ||
    911.                    equalIC(key, this.os()) ||
    912.                    equalIC(key, this.phone()) ||
    913.                    equalIC(key, this.tablet()) ||
    914.                    containsIC(impl.findMatches(impl.mobileDetectRules.utils, this.ua), key);
    915.         },
    916.  
    917.         /**
    918.          * Do a quick test against navigator::userAgent.
    919.          *
    920.          * @param {String|RegExp} pattern the pattern, either as String or RegExp
    921.          *                        (a string will be converted to a case-insensitive RegExp).
    922.          * @returns {boolean} <tt>true</tt> when the pattern matches, otherwise <tt>false</tt>
    923.          * @function MobileDetect#match
    924.          */
    925.         match: function (pattern) {
    926.             if (!(pattern instanceof RegExp)) {
    927.                 pattern = new RegExp(pattern, 'i');
    928.             }
    929.             return pattern.test(this.ua);
    930.         },
    931.  
    932.         /**
    933.          * Checks whether the mobile device can be considered as phone regarding <code>screen.width</code>.
    934.          * <br>
    935.          * Obviously this method makes sense in browser environments only (not for Node.js)!
    936.          * @param {number} [maxPhoneWidth] the maximum logical pixels (aka. CSS-pixels) to be considered as phone.<br>
    937.          *        The argument is optional and if not present or falsy, the value of the constructor is taken.
    938.          * @returns {boolean|undefined} <code>undefined</code> if screen size wasn't detectable, else <code>true</code>
    939.          *          when screen.width is less or equal to maxPhoneWidth, otherwise <code>false</code>.<br>
    940.          *          Will always return <code>undefined</code> server-side.
    941.          */
    942.         isPhoneSized: function (maxPhoneWidth) {
    943.             return MobileDetect.isPhoneSized(maxPhoneWidth || this.maxPhoneWidth);
    944.         },
    945.  
    946.         /**
    947.          * Returns the mobile grade ('A', 'B', 'C').
    948.          *
    949.          * @returns {String} one of the mobile grades ('A', 'B', 'C').
    950.          * @function MobileDetect#mobileGrade
    951.          */
    952.         mobileGrade: function () {
    953.             if (this._cache.grade === undefined) {
    954.                 this._cache.grade = impl.mobileGrade(this);
    955.             }
    956.             return this._cache.grade;
    957.         }
    958.     };
    959.  
    960.     // environment-dependent
    961.     if (typeof window !== 'undefined' && window.screen) {
    962.         MobileDetect.isPhoneSized = function (maxPhoneWidth) {
    963.             return maxPhoneWidth < 0 ? undefined : impl.getDeviceSmallerSide() <= maxPhoneWidth;
    964.         };
    965.     } else {
    966.         MobileDetect.isPhoneSized = function () {};
    967.     }
    968.  
    969.     // should not be replaced by a completely new object - just overwrite existing methods
    970.     MobileDetect._impl = impl;
    971.  
    972.     MobileDetect.version = '1.3.3 2016-07-31';
    973.  
    974.     return MobileDetect;
    975. }); // end of call of define()
    976. })((function (undefined) {
    977.     if (typeof module !== 'undefined' && module.exports) {
    978.         return function (factory) { module.exports = factory(); };
    979.     } else if (typeof define === 'function' && define.amd) {
    980.         return define;
    981.     } else if (typeof window !== 'undefined') {
    982.         return function (factory) { window.MobileDetect = factory(); };
    983.     } else {
    984.         // please file a bug if you get this error!
    985.         throw new Error('unknown environment');
    986.     }
    987. })());
     
    Last edited: Nov 17, 2016
  16. jonas-echterhoff

    jonas-echterhoff

    Unity Technologies

    Joined:
    Aug 18, 2005
    Posts:
    1,666
    No offense taken, and I understand your concerns. And I also agree with you that build size (both code and data) is a huge concern, and actually spend a lot of my time working on things which will help improve this. While we also hope that improvements in web technologies will make this a smaller issue in the future, we want to avoid overpromising on things we cannot directly control, so we hope that we can still improve the situation even with today's tech. This is not an easy task with a big, full-featured engine like Unity, but we are trying to make the code base more modular so more code can be selectively stripped if not used, and to build tooling to give you exact information on which code and which assets contributed to your code and data sizes, and why the have been included.
     
  17. stephanwinterberger

    stephanwinterberger

    Joined:
    Aug 22, 2016
    Posts:
    27
    @danielesuppo: thanks for sharing your experience and code :) one question, are the 6mb the gzipped file size or the uncompressed one? Our main problem is that we need some parts of the physics and therefore the whole physics is in the generated js. And the physics lib is big :( a modular physics lib would be a huge step into the right direction for us.
     
  18. danielesuppo

    danielesuppo

    Joined:
    Oct 20, 2015
    Posts:
    331
    Hi stephanwinterberger, sorry for my late reply.
    6mb are gzipped.
    I'm not using any physics for my project.
    Daniele
     
  19. stephanwinterberger

    stephanwinterberger

    Joined:
    Aug 22, 2016
    Posts:
    27
    @danielesuppo thanks for your response. Our JS is about 5.5MB (gzipped) and 24MB (unzipped). For us stability is not the best. Can you provide a link to your game? If you don't want to post it public you could send me a PM