diff --git a/resources/icons/editor/media.svg b/resources/icons/editor/media.svg
new file mode 100644
index 000000000..0c4feea4c
--- /dev/null
+++ b/resources/icons/editor/media.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m380-300 280-180-280-180v360ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm0-560v560-560Z"/></svg>
\ No newline at end of file
diff --git a/resources/js/wysiwyg/nodes/index.ts b/resources/js/wysiwyg/nodes/index.ts
index a2c739576..669ffe6dd 100644
--- a/resources/js/wysiwyg/nodes/index.ts
+++ b/resources/js/wysiwyg/nodes/index.ts
@@ -1,10 +1,8 @@
 import {HeadingNode, QuoteNode} from '@lexical/rich-text';
 import {CalloutNode} from './callout';
 import {
-    $getNodeByKey,
     ElementNode,
     KlassConstructor,
-    LexicalEditor,
     LexicalNode,
     LexicalNodeReplacement, NodeMutation,
     ParagraphNode
@@ -19,8 +17,8 @@ import {CustomTableNode} from "./custom-table";
 import {HorizontalRuleNode} from "./horizontal-rule";
 import {CodeBlockNode} from "./code-block";
 import {DiagramNode} from "./diagram";
-import {EditorUIManager} from "../ui/framework/manager";
 import {EditorUiContext} from "../ui/framework/core";
+import {MediaNode} from "./media";
 
 /**
  * Load the nodes for lexical.
@@ -40,6 +38,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
         DetailsNode, SummaryNode,
         CodeBlockNode,
         DiagramNode,
+        MediaNode,
         CustomParagraphNode,
         LinkNode,
         {
diff --git a/resources/js/wysiwyg/nodes/media.ts b/resources/js/wysiwyg/nodes/media.ts
new file mode 100644
index 000000000..e0c1b3141
--- /dev/null
+++ b/resources/js/wysiwyg/nodes/media.ts
@@ -0,0 +1,215 @@
+import {
+    DOMConversion,
+    DOMConversionMap, DOMConversionOutput,
+    ElementNode,
+    LexicalEditor,
+    LexicalNode,
+    SerializedElementNode, Spread
+} from 'lexical';
+import type {EditorConfig} from "lexical/LexicalEditor";
+import {el} from "../helpers";
+
+export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
+export type MediaNodeSource = {
+    src: string;
+    type: string;
+};
+
+export type SerializedMediaNode = Spread<{
+    tag: MediaNodeTag;
+    attributes: Record<string, string>;
+    sources: MediaNodeSource[];
+}, SerializedElementNode>
+
+const attributeAllowList = [
+    'id', 'width', 'height', 'style', 'title', 'name',
+    'src', 'allow', 'allowfullscreen', 'loading', 'sandbox',
+    'type', 'data', 'controls', 'autoplay', 'controlslist', 'loop',
+    'muted', 'playsinline', 'poster', 'preload'
+];
+
+function filterAttributes(attributes: Record<string, string>): Record<string, string> {
+    const filtered: Record<string, string> = {};
+    for (const key in Object.keys(attributes)) {
+        if (attributeAllowList.includes(key)) {
+            filtered[key] = attributes[key];
+        }
+    }
+    return filtered;
+}
+
+function domElementToNode(tag: MediaNodeTag, element: Element): MediaNode {
+    const node = $createMediaNode(tag);
+
+    const attributes: Record<string, string> = {};
+    for (const attribute of element.attributes) {
+        attributes[attribute.name] = attribute.value;
+    }
+    node.setAttributes(attributes);
+
+    const sources: MediaNodeSource[] = [];
+    if (tag === 'video' || tag === 'audio') {
+        for (const child of element.children) {
+            if (child.tagName === 'SOURCE') {
+                const src = child.getAttribute('src');
+                const type = child.getAttribute('type');
+                if (src && type) {
+                    sources.push({ src, type });
+                }
+            }
+        }
+        node.setSources(sources);
+    }
+
+    return node;
+}
+
+export class MediaNode extends ElementNode {
+
+    __tag: MediaNodeTag;
+    __attributes: Record<string, string> = {};
+    __sources: MediaNodeSource[] = [];
+
+    static getType() {
+        return 'media';
+    }
+
+    static clone(node: MediaNode) {
+        return new MediaNode(node.__tag, node.__key);
+    }
+
+    constructor(tag: MediaNodeTag, key?: string) {
+        super(key);
+        this.__tag = tag;
+    }
+
+    setTag(tag: MediaNodeTag) {
+        const self = this.getWritable();
+        self.__tag = tag;
+    }
+
+    getTag(): MediaNodeTag {
+        const self = this.getLatest();
+        return self.__tag;
+    }
+
+    setAttributes(attributes: Record<string, string>) {
+        const self = this.getWritable();
+        self.__attributes = filterAttributes(attributes);
+    }
+
+    getAttributes(): Record<string, string> {
+        const self = this.getLatest();
+        return self.__attributes;
+    }
+
+    setSources(sources: MediaNodeSource[]) {
+        const self = this.getWritable();
+        self.__sources = sources;
+    }
+
+    getSources(): MediaNodeSource[] {
+        const self = this.getLatest();
+        return self.__sources;
+    }
+
+    setSrc(src: string): void {
+        const attrs = Object.assign({}, this.getAttributes());
+        if (this.__tag ==='object') {
+            attrs.data = src;
+        } else {
+            attrs.src = src;
+        }
+        this.setAttributes(attrs);
+    }
+
+    setWidthAndHeight(width: string, height: string): void {
+        const attrs = Object.assign(
+            {},
+            this.getAttributes(),
+            {width, height},
+        );
+        this.setAttributes(attrs);
+    }
+
+    createDOM(_config: EditorConfig, _editor: LexicalEditor) {
+        const sources = (this.__tag === 'video' || this.__tag === 'audio') ? this.__sources : [];
+        const sourceEls = sources.map(source => el('source', source));
+
+        return el(this.__tag, this.__attributes, sourceEls);
+    }
+
+    updateDOM(prevNode: unknown, dom: HTMLElement) {
+        return true;
+    }
+
+    static importDOM(): DOMConversionMap|null {
+
+        const buildConverter = (tag: MediaNodeTag) => {
+            return (node: HTMLElement): DOMConversion|null => {
+                return {
+                    conversion: (element: HTMLElement): DOMConversionOutput|null => {
+                        return {
+                            node: domElementToNode(tag, element),
+                        };
+                    },
+                    priority: 3,
+                };
+            };
+        };
+
+        return {
+            iframe: buildConverter('iframe'),
+            embed: buildConverter('embed'),
+            object: buildConverter('object'),
+            video: buildConverter('video'),
+            audio: buildConverter('audio'),
+        };
+    }
+
+    exportJSON(): SerializedMediaNode {
+        return {
+            ...super.exportJSON(),
+            type: 'callout',
+            version: 1,
+            tag: this.__tag,
+            attributes: this.__attributes,
+            sources: this.__sources,
+        };
+    }
+
+    static importJSON(serializedNode: SerializedMediaNode): MediaNode {
+        return $createMediaNode(serializedNode.tag);
+    }
+
+}
+
+export function $createMediaNode(tag: MediaNodeTag) {
+    return new MediaNode(tag);
+}
+
+export function $createMediaNodeFromHtml(html: string): MediaNode | null {
+    const parser = new DOMParser();
+    const doc = parser.parseFromString(`<body>${html}</body>`, 'text/html');
+
+    const el = doc.body.children[0];
+    if (!el) {
+        return null;
+    }
+
+    const tag = el.tagName.toLowerCase();
+    const validTypes = ['embed', 'iframe', 'video', 'audio', 'object'];
+    if (!validTypes.includes(tag)) {
+        return null;
+    }
+
+    return domElementToNode(tag as MediaNodeTag, el);
+}
+
+export function $isMediaNode(node: LexicalNode | null | undefined) {
+    return node instanceof MediaNode;
+}
+
+export function $isMediaNodeOfTag(node: LexicalNode | null | undefined, tag: MediaNodeTag) {
+    return node instanceof MediaNode && (node as MediaNode).getTag() === tag;
+}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md
index c62a6e524..cd36f359e 100644
--- a/resources/js/wysiwyg/todo.md
+++ b/resources/js/wysiwyg/todo.md
@@ -2,17 +2,7 @@
 
 ## In progress
 
-- Add Type: Video/media/embed
-    - TinyMce media embed supported:
-      - iframe
-      - embed
-      - object
-      - video - Can take sources
-      - audio  - Can take sources
-    - Pretty much all attributes look like they were supported.
-    - Core old logic seen here: https://github.com/tinymce/tinymce/blob/main/modules/tinymce/src/plugins/media/main/ts/core/DataToHtml.ts
-    - Copy/store attributes on node based on allow list?
-      - width, height, src, controls, etc... Take valid values from MDN
+- Finish initial media node & form integration
 
 ## Main Todo
 
@@ -31,6 +21,7 @@
 - Image gallery integration for insert
 - Image gallery integration for form
 - Drawing gallery integration
+- Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts)
 
 ## Bugs
 
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
index 88241e926..3c14052ba 100644
--- a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
+++ b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
@@ -23,7 +23,9 @@ import editIcon from "@icons/edit.svg";
 import diagramIcon from "@icons/editor/diagram.svg";
 import {$createDiagramNode, $isDiagramNode, $openDrawingEditorForNode, DiagramNode} from "../../../nodes/diagram";
 import detailsIcon from "@icons/editor/details.svg";
+import mediaIcon from "@icons/editor/media.svg";
 import {$createDetailsNode, $isDetailsNode} from "../../../nodes/details";
+import {$isMediaNode, MediaNode} from "../../../nodes/media";
 
 export const link: EditorButtonDefinition = {
     label: 'Insert/edit link',
@@ -32,7 +34,7 @@ export const link: EditorButtonDefinition = {
         const linkModal = context.manager.createModal('link');
         context.editor.getEditorState().read(() => {
             const selection = $getSelection();
-            const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
+            const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode | null;
 
             let formDefaults = {};
             if (selectedLink) {
@@ -53,7 +55,7 @@ export const link: EditorButtonDefinition = {
             linkModal.show(formDefaults);
         });
     },
-    isActive(selection: BaseSelection|null): boolean {
+    isActive(selection: BaseSelection | null): boolean {
         return $selectionContainsNodeType(selection, $isLinkNode);
     }
 };
@@ -64,7 +66,7 @@ export const unlink: EditorButtonDefinition = {
     action(context: EditorUiContext) {
         context.editor.update(() => {
             const selection = context.lastSelection;
-            const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
+            const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode | null;
             const selectionPoints = selection?.getStartEndPoints();
 
             if (selectedLink) {
@@ -78,20 +80,19 @@ export const unlink: EditorButtonDefinition = {
             }
         });
     },
-    isActive(selection: BaseSelection|null): boolean {
+    isActive(selection: BaseSelection | null): boolean {
         return false;
     }
 };
 
 
-
 export const image: EditorButtonDefinition = {
     label: 'Insert/Edit Image',
     icon: imageIcon,
     action(context: EditorUiContext) {
         const imageModal = context.manager.createModal('image');
         const selection = context.lastSelection;
-        const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode|null;
+        const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode | null;
 
         context.editor.getEditorState().read(() => {
             let formDefaults = {};
@@ -113,7 +114,7 @@ export const image: EditorButtonDefinition = {
             imageModal.show(formDefaults);
         });
     },
-    isActive(selection: BaseSelection|null): boolean {
+    isActive(selection: BaseSelection | null): boolean {
         return $selectionContainsNodeType(selection, $isImageNode);
     }
 };
@@ -126,7 +127,7 @@ export const horizontalRule: EditorButtonDefinition = {
             $insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false);
         });
     },
-    isActive(selection: BaseSelection|null): boolean {
+    isActive(selection: BaseSelection | null): boolean {
         return $selectionContainsNodeType(selection, $isHorizontalRuleNode);
     }
 };
@@ -137,7 +138,7 @@ export const codeBlock: EditorButtonDefinition = {
     action(context: EditorUiContext) {
         context.editor.getEditorState().read(() => {
             const selection = $getSelection();
-            const codeBlock = $getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode|null);
+            const codeBlock = $getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode | null);
             if (codeBlock === null) {
                 context.editor.update(() => {
                     const codeBlock = $createCodeBlockNode();
@@ -151,7 +152,7 @@ export const codeBlock: EditorButtonDefinition = {
             }
         });
     },
-    isActive(selection: BaseSelection|null): boolean {
+    isActive(selection: BaseSelection | null): boolean {
         return $selectionContainsNodeType(selection, $isCodeBlockNode);
     }
 };
@@ -167,7 +168,7 @@ export const diagram: EditorButtonDefinition = {
     action(context: EditorUiContext) {
         context.editor.getEditorState().read(() => {
             const selection = $getSelection();
-            const diagramNode = $getNodeFromSelection(context.lastSelection, $isDiagramNode) as (DiagramNode|null);
+            const diagramNode = $getNodeFromSelection(context.lastSelection, $isDiagramNode) as (DiagramNode | null);
             if (diagramNode === null) {
                 context.editor.update(() => {
                     const diagram = $createDiagramNode();
@@ -180,11 +181,39 @@ export const diagram: EditorButtonDefinition = {
             }
         });
     },
-    isActive(selection: BaseSelection|null): boolean {
+    isActive(selection: BaseSelection | null): boolean {
         return $selectionContainsNodeType(selection, $isDiagramNode);
     }
 };
 
+export const media: EditorButtonDefinition = {
+    label: 'Insert/edit Media',
+    icon: mediaIcon,
+    action(context: EditorUiContext) {
+        const mediaModal = context.manager.createModal('media');
+
+        context.editor.getEditorState().read(() => {
+            const selection = $getSelection();
+            const selectedNode = $getNodeFromSelection(selection, $isMediaNode) as MediaNode | null;
+
+            let formDefaults = {};
+            if (selectedNode) {
+                const nodeAttrs = selectedNode.getAttributes();
+                formDefaults = {
+                    src: nodeAttrs.src || nodeAttrs.data || '',
+                    width: nodeAttrs.width,
+                    height: nodeAttrs.height,
+                    embed: '',
+                }
+            }
+
+            mediaModal.show(formDefaults);
+        });
+    },
+    isActive(selection: BaseSelection | null): boolean {
+        return $selectionContainsNodeType(selection, $isMediaNode);
+    }
+};
 
 export const details: EditorButtonDefinition = {
     label: 'Insert collapsible block',
@@ -209,7 +238,7 @@ export const details: EditorButtonDefinition = {
             }
         });
     },
-    isActive(selection: BaseSelection|null): boolean {
+    isActive(selection: BaseSelection | null): boolean {
         return $selectionContainsNodeType(selection, $isDetailsNode);
     }
 }
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/defaults/form-definitions.ts b/resources/js/wysiwyg/ui/defaults/form-definitions.ts
index 04147a4f0..e0459b5c5 100644
--- a/resources/js/wysiwyg/ui/defaults/form-definitions.ts
+++ b/resources/js/wysiwyg/ui/defaults/form-definitions.ts
@@ -4,6 +4,7 @@ import {$createLinkNode} from "@lexical/link";
 import {$createTextNode, $getSelection} from "lexical";
 import {$createImageNode} from "../../nodes/image";
 import {setEditorContentFromHtml} from "../../actions";
+import {$createMediaNodeFromHtml} from "../../nodes/media";
 
 
 export const link: EditorFormDefinition = {
@@ -89,6 +90,55 @@ export const image: EditorFormDefinition = {
     ],
 };
 
+export const media: EditorFormDefinition = {
+    submitText: 'Save',
+    action(formData, context: EditorUiContext) {
+
+        // TODO - Get media from selection
+
+        const embedCode = (formData.get('embed') || '').toString().trim();
+        if (embedCode) {
+            context.editor.update(() => {
+                const node = $createMediaNodeFromHtml(embedCode);
+                // TODO - Replace existing or insert new
+            });
+
+            return true;
+        }
+
+        const src = (formData.get('src') || '').toString().trim();
+        const height = (formData.get('height') || '').toString().trim();
+        const width = (formData.get('width') || '').toString().trim();
+
+        // TODO - Update existing or insert new
+
+        return true;
+    },
+    fields: [
+        {
+            label: 'Source',
+            name: 'src',
+            type: 'text',
+        },
+        {
+            label: 'Width',
+            name: 'width',
+            type: 'text',
+        },
+        {
+            label: 'Height',
+            name: 'height',
+            type: 'text',
+        },
+        // TODO - Tabbed interface to separate this option
+        {
+            label: 'Paste your embed code below:',
+            name: 'embed',
+            type: 'textarea',
+        },
+    ],
+};
+
 export const source: EditorFormDefinition = {
     submitText: 'Save',
     action(formData, context: EditorUiContext) {
diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts
index 5dee6b62b..a3f150e52 100644
--- a/resources/js/wysiwyg/ui/index.ts
+++ b/resources/js/wysiwyg/ui/index.ts
@@ -6,7 +6,7 @@ import {
     getMainEditorFullToolbar, getTableToolbarContent
 } from "./toolbars";
 import {EditorUIManager} from "./framework/manager";
-import {image as imageFormDefinition, link as linkFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions";
+import {image as imageFormDefinition, link as linkFormDefinition, media as mediaFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions";
 import {ImageDecorator} from "./decorators/image";
 import {EditorUiContext} from "./framework/core";
 import {CodeBlockDecorator} from "./decorators/code-block";
@@ -38,6 +38,10 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
         title: 'Insert/Edit Image',
         form: imageFormDefinition
     });
+    manager.registerModal('media', {
+        title: 'Insert/Edit Media',
+        form: mediaFormDefinition,
+    });
     manager.registerModal('source', {
         title: 'Source code',
         form: sourceFormDefinition,
diff --git a/resources/js/wysiwyg/ui/toolbars.ts b/resources/js/wysiwyg/ui/toolbars.ts
index 5d40578a5..ae6a292a2 100644
--- a/resources/js/wysiwyg/ui/toolbars.ts
+++ b/resources/js/wysiwyg/ui/toolbars.ts
@@ -47,7 +47,7 @@ import {
     editCodeBlock,
     horizontalRule,
     image,
-    link,
+    link, media,
     unlink
 } from "./defaults/buttons/objects";
 
@@ -110,7 +110,7 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
         ]),
 
         // Insert types
-        new EditorOverflowContainer(6, [
+        new EditorOverflowContainer(8, [
             new EditorButton(link),
             new EditorDropdownButton(table, false, [
                 new EditorTableCreator(),
@@ -119,6 +119,7 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
             new EditorButton(horizontalRule),
             new EditorButton(codeBlock),
             new EditorButton(diagram),
+            new EditorButton(media),
             new EditorButton(details),
         ]),