diff --git a/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md b/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md index 0efc89d..b40d571 100644 --- a/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md +++ b/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md @@ -76,21 +76,4 @@ There are two ways to set metadata for your tokens: ``` Whichever method you choose, it's crucial to ensure the metadata is structured according to the standard to maintain compatibility and present your tokens correctly. -For a detailed guide on adding metadata, please refer to our [Adding Metadata Tutorial](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md). - -### Dynamic Metadata Fetching - -When an application such as [Enjin wallet](https://enj.in/wallet) / [NFT.io](https://nft.io) loads token metadata, it starts by looking for the `uri` attribute on the token level. -If there’s no `uri` on the token level, it then looks for the `uri` attribute on the collection level. - -While each token could have it’s own uri attribute, It is sometimes more convenient, and efficient, to use Dynamic Metadata. - -For Dynamic Metadata fetching, the metadata doesn't exist on each token. -Instead, one `uri` attribute is set on the collection level, with the dynamic keyword `{id}.json`. -For example: `https://yourdomain.com/{id}.json` - -When fetching a token dynamically, the `{id}` is replaced with the collection ID, followed by the token ID, like this: https://yourdomain.com/8143-72.json. - -Dynamic Metadata works best for NFTs with large supply, as usually each NFT has it’s own Metadata, and by using Dynamic Metadata there’s less data stored on-chain, drastically reducing the amount of ENJ tokens required when creating the tokens. - -For a detailed step-by-step guide on setting Dynamic Metadata Fetching, please refer to our Help Center article: [Creating and Hosting Metadata Dynamically](https://support.enjin.io/hc/en-gb/articles/16617419735314-Creating-and-Hosting-Metadata-Dynamically) +For a detailed guide on adding metadata, please refer to our [Adding Metadata Tutorial](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md). \ No newline at end of file diff --git a/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md b/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md new file mode 100644 index 0000000..0e7a7cb --- /dev/null +++ b/docs/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/03-dynamic-metadata.md @@ -0,0 +1,188 @@ +--- +title: "Dynamic Metadata" +slug: "dynamic-metadata" +description: "Learn to use Dynamic Metadata to efficiently manage large NFT collections. Our easy-to-follow guide shows how to set one URL for all your tokens, saving time and on-chain costs." +--- + +What if you have a collection with 10,000 unique tokens? Setting up metadata (like the name, image, and description) for each one sounds like a lot of work, and it can be costly. + +**Dynamic metadata** is our simple and efficient solution for this. + +Instead of putting metadata on *every single token*, you just set **one link** on your entire **collection**. Applications like the Enjin Wallet and NFT.io marketplace will then use that single link to automatically find the correct metadata for each specific token as it's needed. + +### 🎯 Why Use Dynamic Metadata? + + * **Save Time & Effort:** Manage metadata for thousands (or millions\!) of tokens from one central place. + * **Save on Costs:** This method uses much less on-chain data, which can reduce the ENJ required to create your tokens. + * **Easy to Update:** Want to change your token's image? Just replace the media file on your server. There's no need for a new on-chain transaction. + +----- + +## How Dynamic Metadata Works + +Understanding this is key to setting it up correctly. + +### How Your Metadata is Found (The Hierarchy) + +When you look at an NFT in your wallet, the wallet needs to find its image and name. It searches in a specific order: + +1. **Token:** First, it checks for metadata *directly on that single token*. +2. **Group (Optional):** If not, it checks if the token is in a *group* that has metadata. +3. **Collection:** If it's still empty, it finally looks at the *whole collection* for the metadata link. + +Dynamic metadata works by setting the link at the **Collection level** (step 3). This way, all tokens in that collection will use this link as their default. + +### The Magic `{id}` Placeholder + +This is the most important part. The single link you set on your collection will include the special placeholder: `{id}.json` + +> **Example link:** `https://my-server.com/metadata/{id}.json` + +When a wallet needs to find a token's details, it automatically replaces `{id}` with the correct filename. Your job is to make sure your files are hosted and named correctly so the wallet can find them. + + +### Let's Walk Through an Example + +Seeing this in action makes it much clearer. + +Let's imagine you have a collection called **"Hero's Hoard"** which has an ID of **`8143`**. +Inside this collection, you have a token called **"Dragon's Fang"** with an ID of **`72`**. + +You have already set up dynamic metadata for your collection, and the `uri` attribute on your collection (`8143`) is: +`ipfs://bafy...xyz/{id}.json` + +Now, a user opens the NFT.io marketplace to look at your "Dragon's Fang" token. Here's what happens behind the scenes: + +1. **NFT.io needs metadata for token `72` in collection `8143`.** +2. It checks the token itself for a `uri`. **Nothing is set there.** +3. It checks the token's group (if any) for a `uri`. **Nothing is set there either.** +4. It checks the *collection* (`8143`) for a `uri`. **Success!** It finds your dynamic link: `ipfs://bafy...xyz/{id}.json`. +5. NFT.io knows it's looking for a *token*, so it uses the token file name convention: `-.json`. +6. It replaces the `{id}` in your link with **`8143-72`**. +7. The final URL it fetches is: `ipfs://bafy...xyz/8143-72.json`. + +Your server (or Pinata, in our example) then sends this specific file, which contains the name "Dragon's Fang" and its unique image. That's how it all connects! + +### What You Must Name Your Files + +For this to work, your hosted files **must** use these exact naming conventions: + + * **For your Collection's metadata:** `.json` + * *Example: `8143.json`* + * **For each Token (NFT) metadata:** `-.json` + * *Example: `8143-72.json` (This is for Token 72 in Collection 8143)* + * **For a Token Group metadata (Optional):** `-group.json` + * *Example: `5-group.json`* + +----- + +## How to Set Up Dynamic Metadata: A Step-by-Step Guide + +This setup is a three-step process. We'll walk you through it using a free, easy-to-use service called [Pinata.cloud](https://www.pinata.cloud/), which hosts files on a decentralized network called IPFS. + +:::tip Do I have to use IPFS? +You can use any public server or hosting service, but Pinata is a great choice for beginners. +::: + +### Step 1: Host Your Media Files (Images, Videos, etc.) + +First, let's get all your images and videos uploaded. + +1. Sign up for a free account at [Pinata.cloud](https://www.pinata.cloud/). +2. On your computer, create a folder and put all your media files into it (e.g., `collection-banner.png`, `token-1.jpg`, `token-2.mp4`). +3. On your Pinata dashboard, click the blue "**+ Add**" button in the top-right corner, then select **Folder Upload**. +4. Upload the folder you just prepared. +5. Once it's finished, you'll see your folder in the file list. Copy its **CID** (a long string of letters and numbers). +6. Save this CID somewhere safe (like a notepad). This is your ``. + + + +### Step 2: Create and Host Your JSON Metadata Files + +Next, we'll create simple text files (called JSON files) that tell the wallet *about* your tokens, including where to find the media files you just uploaded. + +1. **Create your Collection JSON file:** + + * On your computer, create a new text file and name it *exactly* `.json` (e.g., `8143.json`). + * Inside this file, add your collection's details like `name` and `description`, following the [Metadata Standard](/02-guides/01-platform/03-advanced-mechanics/02-metadata-standard/02-metadata-standard.md). + * For the `media` or `image` field, add the URL to your collection's media file from Step 1. The format is: `ipfs:///filename.png` + * *Example:* `"url": "ipfs://bafy...abc/collection-banner.png"` + +2. **Create your Token JSON files:** + + * Now, do the same for *each* token. Create a new text file and name it *exactly* `-.json` (e.Example: `8143-72.json`). + * Inside, add that token's specific `name`, `description`, `attributes`, etc. + * For its `media`, point to its specific media file: `ipfs:///token-72.jpg` + * *Example:* `"url": "ipfs://bafy...abc/token-72.jpg"` + +3. **Create your Group JSON files (Optional):** + + * If you use Token Groups, repeat the process. Name the file `-group.json` (e.Example: `5-group.json`). + +4. **Upload Your JSON Folder:** + + * Once you've created all your `.json` files, put them *all* into a **new, separate folder** on your computer. + * Go back to Pinata and upload this new folder (the one containing all your `.json` files). + * When it's done, copy the **CID** for this **JSON folder**. This is the final piece you need! Let's call it your ``. + +### Step 3: Update Your Collection's On-Chain URI + +Finally, let's tell your collection where to find these new metadata files. You'll do this by setting its `uri` attribute. + +1. The `value` you need to set is your **``** from the last step, followed by the `/{id}.json` placeholder. + * **Example Value:** `ipfs://bafy...xyz/{id}.json` (Replace `bafy...xyz` with your actual ``). +2. Set this attribute using the `BatchSetAttribute` mutation. You can copy/paste the example below into the [graphiql playground](https://platform.enjin.io/graphiql), and change the values. + +```graphql +mutation SetDynamicMetadataURI { + BatchSetAttribute( + collectionId: 8143 # 👈 Change this to your Collection ID + # We're not setting a tokenId, so this applies to the whole collection + attributes: [ + { + key: "uri" + # 👇 Change this to your JSON folder's CID + the {id} placeholder + value: "ipfs://bafy...xyz/{id}.json" + } + ] + ) { + id + state + } +} +``` + +3. Run this mutation, and you're all set\! Wallets and marketplaces will now automatically find your metadata. + +:::warning **Important:** Do Not Replace `{id}` +You must use the literal string `{id}.json` at the end of your URI. **Do not** replace `{id}` with an actual ID. The wallet or marketplace handles this replacement for you. +::: + +:::info +For a more detailed guide on updating metadata using the Enjin Platform, check out [this page](/02-guides/01-platform/01-managing-tokens/03-adding-metadata.md). +::: + +----- + +## Migrating Legacy Metadata (For collections from Ethereum/JumpNet) + +This section is **only** if you are moving a collection from Ethereum or JumpNet and *already* used dynamic metadata there. The token ID format has changed, so you just need to rename your old `.json` files. + +### Conversion Steps + +1. Find the new **Collection ID** for your collection on Enjin Blockchain (e.g., `2000`). +2. Create a new JSON file for your collection, named `.json` (e.g., `2000.json`). +3. Now, for each token, find its **old** `.json` filename. + * *Old Example:* `70800000000000af000000000000000000000000000000000000000000000005.json` +4. **Follow these steps to get the new name:** + 1. **Remove the 32 zeros** from the middle (from position 17 to 48). *Result:* `70800000000000af0000000000000005.json` + 2. **Remove the `.json`** at the end. *Result:* `70800000000000af0000000000000005` + 3. **Convert this number.** This is a "hexadecimal" number. Use any online "hex to decimal" converter to change it. *Result:* `149538149525803038929858507180710297605` + 4. **Add `.json` back** to the end. **New Filename:** `149538149525803038929858507180710297605.json` +5. Repeat this renaming process for all your old token JSON files. +6. Upload all your **newly renamed** files (plus your new collection file, e.g., `2000.json`) into a new folder on your server or IPFS. +7. Finally, update your collection's `uri` attribute (like in [Step 3 above](#step-3-update-your-collections-on-chain-uri)) to point to this new folder, making sure to add `{id}.json` at the end. + * *Example:* `httpss://example.com/metadata/{id}.json` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7cca502..82033ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -161,6 +161,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.23.4.tgz", "integrity": "sha512-uBGo6KwUP6z+u6HZWRui8UJClS7fgUIAiYd1prUqCbkzDiCngTOzxaJbEvrdkK0hGCQtnPDiuNhC5MhtVNN4Eg==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.23.4", "@algolia/requester-browser-xhr": "5.23.4", @@ -299,6 +300,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -2064,6 +2066,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2086,6 +2089,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2166,6 +2170,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2500,6 +2505,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3361,6 +3367,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.7.0.tgz", "integrity": "sha512-GXg5V7kC9FZE4FkUZA8oo/NrlRb06UwuICzI6tcbzj0+TVgjq/mpUXXzSgKzMS82YByi4dY2Q808njcBCyy6tQ==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/core": "3.7.0", "@docusaurus/logger": "3.7.0", @@ -3862,6 +3869,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -3905,6 +3913,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -4099,6 +4108,7 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -4631,6 +4641,7 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -5026,6 +5037,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -5367,6 +5379,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5422,6 +5435,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5467,6 +5481,7 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.23.4.tgz", "integrity": "sha512-QzAKFHl3fm53s44VHrTdEo0TkpL3XVUYQpnZy1r6/EHvMAyIg+O4hwprzlsNmcCHTNyVcF2S13DAUn7XhkC6qg==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-abtesting": "5.23.4", "@algolia/client-analytics": "5.23.4", @@ -5960,6 +5975,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -6906,6 +6922,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -8318,6 +8335,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -8527,6 +8545,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -12977,6 +12996,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13539,6 +13559,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -14442,6 +14463,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -15235,6 +15257,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -15370,6 +15393,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -15419,6 +15443,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" }, @@ -15447,6 +15472,7 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -17649,6 +17675,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -17847,6 +17874,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.6.tgz", "integrity": "sha512-TJOLrJ6oeccsGWPl7ujCYuc0pIq2cNsuD6GZDma8i5o5Npvcco/z+NKvZSFsP0/x6SShVb0+X2JK/JHUjKY9dQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", diff --git a/static/img/guides/advanced-mechanics/uploading-media-files.mp4 b/static/img/guides/advanced-mechanics/uploading-media-files.mp4 new file mode 100644 index 0000000..a5afd78 Binary files /dev/null and b/static/img/guides/advanced-mechanics/uploading-media-files.mp4 differ